diff --git a/modules/mesh-vault/README.md b/modules/mesh-vault/README.md index c633a034..75b089c7 100644 --- a/modules/mesh-vault/README.md +++ b/modules/mesh-vault/README.md @@ -136,7 +136,7 @@ the declaration sealed by the vault to their machine, which the controller carri machine's sealing key as its own node-engine stated it in its own name, in `move-in`'s first step (`sealing_key`). A machine whose engine stated none, any other key, any other module and any other name are refused and recorded. -## Taking values in: `accept`, `secret-ask`, `issued`, `rotate` +## Taking values in: `secret-ask`, `issued`, `rotate` Design 57's V2 (issue 675; ADR 0349 clauses 8 to 10, ADR 0277 §22–§27 and ADR 0305 with the vault in the controller's place). `intake.go`. @@ -156,12 +156,8 @@ controller still asks them itself for a module filled the old way, until R1 (iss answer both until then. Everything the vault keeps is its account's: its directories, the export it keeps, the grant files the controller still writes for it. -- `accept` — two steps: node, module, name (a module's own secret) or provider, provision (a provision's one - credential), and `by`; then `call` and `sealed`. The controller's terminal seals the value it read to the call's - key before it leaves the terminal. Kept with origin `accepted`; an accepted provision credential is never remade - (`rotate` refuses it); a value a provider's software issued is not taken by `accept`. -- `secret-ask` — one step: node, module, name, desk and `asked_by` (who asked the controller, as the bus named its - caller). The ask is recorded in `asks.jsonl` before anything else, and refused while another ask for the same +- `secret-ask` — one step: node, module, name and desk, asked by a caller the vault's manifest names (`accept`, and + the controller's relay of who asked it, went with the controller, ADR 0590). The ask is recorded in `asks.jsonl` before anything else, and refused while another ask for the same secret is open (two minutes at most) or when three were made in the last hour. The vault then asks the desk's prompt itself, naming who asked as the record has it, with `timeout_seconds` 20. The answer is how it ended (`given`, dismissed, timed-out, empty, refused, failed) and who asked; how each ask ended is recorded. diff --git a/modules/mesh-vault/cmd/mesh-vault/core.go b/modules/mesh-vault/cmd/mesh-vault/core.go index 985fe26d..b0264c61 100644 --- a/modules/mesh-vault/cmd/mesh-vault/core.go +++ b/modules/mesh-vault/cmd/mesh-vault/core.go @@ -20,7 +20,6 @@ import ( "fmt" "os" "path/filepath" - "slices" "strings" "time" ) @@ -103,40 +102,3 @@ func (v *Vault) recordNodeKey(node, key, caller string) error { } return nil } - -// coreRefusal says why the controller may not have name sealed to key, or "" when it may: name is a core module's -// own secret and key is its machine's sealing key as that machine's engine stated it. -func (v *Vault) coreRefusal(name, key string) string { - parts := strings.Split(name, "/") - if len(parts) != 4 || parts[0] != "own" { - return fmt.Sprintf("the controller is handed a core module's own secret alone, own//<%s>/, sealed "+ - "to its machine (ADR 0349 §16); %s is not one", strings.Join(CoreModules, "|"), name) - } - node, module := parts[1], parts[2] - if !slices.Contains(CoreModules, module) { - return fmt.Sprintf("%s is not one of the core's modules (%s): its secrets reach its machine through the "+ - "provisioner or its declaration, never through the controller's ask (ADR 0349 §16)", module, - strings.Join(CoreModules, ", ")) - } - stated, held, err := v.Store.NodeKey(node) - if err != nil { - return fmt.Sprintf("%s's sealing key cannot be read: %v", node, err) - } - if !held { - return fmt.Sprintf("%s's node-engine has stated no sealing key to the vault (it does so at the move), so "+ - "there is no key of that machine's own to seal %s to", node, name) - } - want, err := publicPEM(stated) - if err != nil { - return fmt.Sprintf("%s's sealing key as recorded is not usable: %v", node, err) - } - named, err := askerKey(key) - if err != nil { - return err.Error() - } - if named != want { - return fmt.Sprintf("%s is sealed only to %s's sealing key as its node-engine stated it, and the key named "+ - "is another", name, node) - } - return "" -} diff --git a/modules/mesh-vault/cmd/mesh-vault/core_test.go b/modules/mesh-vault/cmd/mesh-vault/core_test.go index bbc6826e..5ea24183 100644 --- a/modules/mesh-vault/cmd/mesh-vault/core_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/core_test.go @@ -20,7 +20,6 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/seal" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" ) @@ -205,74 +204,3 @@ authorization { type errString string func (e errString) Error() string { return string(e) } - -func TestOnARealBusTheControllerGetsACoreCredentialSealedOnlyToItsNodesKey(t *testing.T) { - ask, anchorPub, anchorPriv, heard := coreOnABus(t) - - got, err := ask("own/anchor/mesh-provisioner-client/provisioner-broker", pemOf(t, anchorPub)) - if err != nil { - t.Fatal(err) - } - if got.Name != "own/anchor/mesh-provisioner-client/provisioner-broker" || got.Generation != 1 { - t.Errorf("answered %+v", got) - } - opened, err := seal.Open(got.Sealed, anchorPriv) - if err != nil || opened != brokerValue { - t.Fatalf("anchor's key opens %q, %v", opened, err) - } - - // The controller's own key, or any key but anchor's, is refused: it carries the box and never opens it. - mine, err := seal.GenerateKeyPair() - if err != nil { - t.Fatal(err) - } - if got, err := ask("own/anchor/mesh-provisioner-client/provisioner-broker", mine.PublicKey); err == nil { - t.Errorf("the controller was handed anchor's core credential sealed to its own key: %+v", got) - } else if !strings.Contains(err.Error(), "sealing key") { - t.Errorf("refused for another reason: %v", err) - } - // A module that is not the core's is refused, though anchor's key is the one named. - if got, err := ask("own/anchor/postgres/superuser-password", pemOf(t, anchorPub)); err == nil { - t.Errorf("the controller was handed a leaf module's secret: %+v", got) - } else if !strings.Contains(err.Error(), "core") { - t.Errorf("refused for another reason: %v", err) - } - // A machine whose engine stated no key: nothing to seal to, so nothing is handed. - laptopPub, _ := aSealingKey(t) - if _, err := ask("own/laptop/nats/broker", pemOf(t, laptopPub)); err == nil { - t.Error("the controller was handed a core credential for a machine whose engine stated no key") - } - - all := heard() - if !strings.Contains(all, "mesh.seat.mesh-vault.call.get.controller") { - t.Fatalf("the listener heard nothing of the asks:\n%s", all) - } - for _, v := range []string{brokerValue, movedValue} { - for _, form := range []string{v, base64.StdEncoding.EncodeToString([]byte(v))} { - if strings.Contains(all, form) { - t.Fatal("a value crossed the bus in the clear") - } - } - } -} - -// Only the controller is answered by this rule: a machine's client asking for a core credential, which no plan -// names, is still refused by the running rule, and a node-engine is not an asker. -func TestTheCoreRuleAnswersTheControllerAlone(t *testing.T) { - v := newVault(t, Declared{Read: assignments{}.read}) - pub, _ := aSealingKey(t) - if _, err := moveIn(t, v, "node.anchor", clientsBroker(pub), brokerValue); err != nil { - t.Fatal(err) - } - for _, caller := range []string{"provisioner.anchor", "node.anchor", "anchor.mesh-provisioner-client"} { - if _, err := v.Get(caller, "own/anchor/mesh-provisioner-client/provisioner-broker", pemOf(t, pub)); err == nil { - t.Errorf("%s was handed a core credential", caller) - } - } - // The controller is answered for nothing else: not a pair credential, not a provision's. - for _, name := range []string{"pair/anchor/postgres/anchor/gitea", "provision/anchor/postgres"} { - if _, err := v.Get(ControllerCaller, name, pemOf(t, pub)); err == nil { - t.Errorf("the controller was handed %s", name) - } - } -} diff --git a/modules/mesh-vault/cmd/mesh-vault/intake.go b/modules/mesh-vault/cmd/mesh-vault/intake.go index 9e76e0b6..f37d4378 100644 --- a/modules/mesh-vault/cmd/mesh-vault/intake.go +++ b/modules/mesh-vault/cmd/mesh-vault/intake.go @@ -86,8 +86,6 @@ func ProvisionSecret(provider, provision string) (string, error) { // callKey is one call's key and what the call takes in. Never written anywhere. type callKey struct { verb, secret, by string - // origin is what an `accept` call keeps the value as: accepted, or issued for a covered account (issue 787). - origin string // 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). @@ -107,19 +105,6 @@ type Opened struct { AskedBy string `json:"asked_by,omitempty"` } -// fromTheController refuses every caller but the controller: only it asks the vault to take a value in, and only -// it carries a value given at its terminal. -func fromTheController(caller, verb string) error { - if caller != ControllerCaller { - if caller == "" { - caller = stdio.Unproven - } - return fmt.Errorf("%s takes a value from the controller alone, which carries it sealed to this vault's key "+ - "for one call; %s is not the controller", verb, caller) - } - return nil -} - // fromAGrantedCaller refuses a caller the runtime did not prove: `rotate` and `issued` answer every caller whose // manifest invokes them, which the bus grants it (novox/hq ADR 0570, Consequences), no longer the controller alone. func fromAGrantedCaller(caller, verb string) error { @@ -258,80 +243,6 @@ func NamedAccount(secret string) bool { return len(parts) == 4 && parts[0] == "own" && (parts[3] == "broker" || parts[3] == "provisioner-broker") } -// AcceptOpen opens a call to take a value given at the controller's terminal for secret, which the terminal -// seals to the call's key before it leaves it (ADR 0349 clause 8: the vault alone opens an accepted value). -// -// **With origin issued it is a covered account the controller minted** (ADR 0374, issue 787): the controller alone -// hands it, for a covered secret alone, and a re-issue replaces the value in place. Without origin it is a value -// given at the terminal, as before. -func (v *Vault) AcceptOpen(caller, secret, by, origin string) (Opened, error) { - if err := fromTheController(caller, "accept"); err != nil { - return Opened{}, err - } - if err := validName(secret); err != nil { - return Opened{}, err - } - kept := OriginAccepted - switch origin { - case "": - if rec, held, err := v.Store.Read(secret); err != nil { - return Opened{}, err - } else if held && rec.Origin == OriginIssued && !Covered(secret) { - return Opened{}, fmt.Errorf("%s is issued by its provider's software, and `secret issued` is the only way "+ - "it enters the vault (ADR 0305 §10); nothing is taken", secret) - } - case OriginIssued: - if !Covered(secret) { - return Opened{}, fmt.Errorf("%s is not a bus account the controller mints for a core or moved module "+ - "(%s), so it is not handed with origin issued; nothing is taken", secret, strings.Join(CoveredSecrets, ", ")) - } - kept = OriginIssued - default: - return Opened{}, fmt.Errorf("the origin %q is not one accept takes (issued, or none for a value given at the "+ - "terminal); nothing is taken", origin) - } - id, key, err := v.openCall("accept", secret, by, nil) - if err != nil { - return Opened{}, err - } - v.callsMu.Lock() - if c := v.pending[id]; c != nil { - c.origin = kept - } - v.callsMu.Unlock() - return Opened{Call: id, SealTo: key, Secret: secret}, nil -} - -// AcceptTake opens the value sealed to the call's key and keeps it, accepted: never remade by the vault. -func (v *Vault) AcceptTake(caller, call, sealed string) (Changed, error) { - if err := fromTheController(caller, "accept"); err != nil { - return Changed{}, err - } - c, err := v.takeCall("accept", call) - if err != nil { - return Changed{}, err - } - value, err := c.open(sealed) - if err != nil { - return Changed{}, err - } - defer zero(value) - if strings.TrimSpace(string(value)) == "" { - return Changed{}, errors.New("the value given is empty; nothing was taken") - } - origin := c.origin - if origin == "" { - origin = OriginAccepted - } - if origin == OriginIssued { - // **The vault's mint wins** (issue 774): during the delivery a controller older than the hand-over may still mint - // a covered account and hand it here; it never replaces an account the vault minted, judged under the store's - // lock, so the value and the hash the bus loaded stay the vault's. - return v.keepWith(c.secret, string(value), someoneOr(c.by, caller), caller, origin, keepOptions{refuse: v.vaultMinted}) - } - return v.keep(c.secret, string(value), someoneOr(c.by, caller), caller, origin) -} - // vaultMinted refuses a value handed for a secret holding an account the vault minted itself (mint.go): one is never // replaced but by the vault's own rotation. func (v *Vault) vaultMinted(before Record) string { @@ -351,26 +262,19 @@ func (v *Vault) vaultMinted(before Record) string { // AskOf is what an ask is for, and where. type AskOf struct { Node, Module, Name, Desk string - // AskedBy is who asked the controller, as the bus named its caller; the controller's word, taken from the - // controller alone. A caller asking directly is named by the runtime's proof, never by this (ADR 0563 §8). - AskedBy string } -// askedByFor is who asked a secret-ask as the bus says it (novox/hq ADR 0563 §8): for a call relayed by the controller, -// the caller the bus named to the controller, which it takes from the controller alone, as before; for a caller the -// manifest names, that caller as the runtime proved it. Any other caller is refused: no caller names itself. +// askedByFor is who asked a secret-ask as the bus says it (novox/hq ADR 0563 §8): a caller the manifest names, as the +// runtime proved it. Any other caller is refused: no caller names itself, and none relays another's word. func (v *Vault) askedByFor(caller string, a AskOf) (string, error) { - switch { - case caller == ControllerCaller: - return askedByName(a.AskedBy), nil - case v.AskCallers.Allows(caller): + if v.AskCallers.Allows(caller) { return askedByName(caller), nil } if caller == "" { caller = stdio.Unproven } - return "", fmt.Errorf("secret-ask answers the controller and the callers the vault's manifest names, and %s is "+ - "neither; nobody was asked to type anything", caller) + return "", fmt.Errorf("secret-ask answers the callers the vault's manifest names, and %s is not one; nobody was "+ + "asked to type anything", caller) } // AskOpen records an ask for a module's own secret before any prompt opens, holding its bounds — one open ask @@ -787,9 +691,7 @@ func (v *Vault) RotateFor(caller, secret string, one Narrow, by, why string) (Ch if err := fromAGrantedCaller(caller, "rotate"); err != nil { return Changed{}, err } - if caller != ControllerCaller { - by = "" // only the controller relays who asked at its terminal; any other caller is who asked - } + by = "" // who asked is the caller the runtime proved: no caller relays another's word (novox/hq ADR 0590) if strings.TrimSpace(why) == "" { return Changed{}, errors.New("not rotated: a rotation says why it was asked (`why`)") } diff --git a/modules/mesh-vault/cmd/mesh-vault/intake_test.go b/modules/mesh-vault/cmd/mesh-vault/intake_test.go index 6f25174d..610b84d0 100644 --- a/modules/mesh-vault/cmd/mesh-vault/intake_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/intake_test.go @@ -75,7 +75,7 @@ func everything(t *testing.T, v testVault, more ...any) string { const typedAtTheDesk = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g" func telegramAsk(by string) AskOf { - return AskOf{Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: by} + return AskOf{Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop"} } // ---- the vault's secret-ask test (ADR 0349 "How it is checked") --------------------------------------------- @@ -84,33 +84,34 @@ func telegramAsk(by string) AskOf { // it; the typed value opens only in the vault; the controller carries ciphertext only. func TestAnAskIsRecordedBeforeThePromptAndTheTypedValueOpensOnlyInTheVault(t *testing.T) { v := newVault(t, AllowList{"anchor.node-provisioner": {"own/anchor/telegram/telegram-token"}}) - opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14/claude-code, via the console")) + v.AskCallers = stdio.Callers{"g14.claude-code"} + opened, err := v.AskOpen("g14.claude-code", telegramAsk("g14/claude-code, via the console")) if err != nil { t.Fatal(err) } // Recorded before any prompt opens: the record holds the ask now, with no end. asks, _ := v.Store.Asks(10) - if len(asks) != 1 || asks[0].Event != AskOpened || asks[0].ID != opened.Call || asks[0].AskedBy != "g14/claude-code" || - asks[0].Caller != ControllerCaller || asks[0].Desk != "laptop" || asks[0].Secret != "own/anchor/telegram/telegram-token" { + if len(asks) != 1 || asks[0].Event != AskOpened || asks[0].ID != opened.Call || asks[0].AskedBy != "g14.claude-code" || + asks[0].Caller != "g14.claude-code" || asks[0].Desk != "laptop" || asks[0].Secret != "own/anchor/telegram/telegram-token" { t.Fatalf("the record before the prompt: %+v", asks) } // The prompt names the asker as the bus named it, cut to a name: what the vault answers is what it recorded. - if opened.AskedBy != "g14/claude-code" { + if opened.AskedBy != "g14.claude-code" { t.Errorf("the prompt would name %q", opened.AskedBy) } // The desk seals what is typed to the call's key; the controller passes it on. - changed, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, typedAtTheDesk+"\n"), "") + changed, err := v.AskEnd("g14.claude-code", opened.Call, sealToCall(t, opened.SealTo, typedAtTheDesk+"\n"), "") if err != nil { t.Fatal(err) } - if changed.Generation != 1 || changed.By != "g14/claude-code" { + if changed.Generation != 1 || changed.By != "g14.claude-code" { t.Errorf("kept as %+v", changed) } if got := handedTo(t, v, "anchor.node-provisioner", "own/anchor/telegram/telegram-token"); got != typedAtTheDesk { t.Errorf("the vault holds %q, not what was typed with its line ending taken off", got) } rec, _, _ := v.Store.Read("own/anchor/telegram/telegram-token") - if rec.Origin != OriginTyped || rec.ChangedBy != "g14/claude-code" || rec.ChangedVia != ControllerCaller { + if rec.Origin != OriginTyped || rec.ChangedBy != "g14.claude-code" || rec.ChangedVia != "" { t.Errorf("the record says origin %q, by %q, via %q", rec.Origin, rec.ChangedBy, rec.ChangedVia) } asks, _ = v.Store.Asks(10) @@ -125,33 +126,34 @@ func TestAnAskIsRecordedBeforeThePromptAndTheTypedValueOpensOnlyInTheVault(t *te // One open ask per secret, and three an hour (ADR 0277 §24), read from the vault's own record. func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) { v := newVault(t, nil) - first, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + v.AskCallers = stdio.Callers{"g14.claude-code"} + first, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")) if err != nil { t.Fatal(err) } - if _, err := v.AskOpen(ControllerCaller, telegramAsk("ace.claude-code")); err == nil || + if _, err := v.AskOpen("g14.claude-code", telegramAsk("ace.claude-code")); err == nil || !strings.Contains(err.Error(), "still open (asked by g14.claude-code)") { t.Fatalf("a second ask while one is open: %v", err) } // Another secret is not held back by it. other := telegramAsk("ace.claude-code") other.Name = "webhook-secret" - if _, err := v.AskOpen(ControllerCaller, other); err != nil { + if _, err := v.AskOpen("g14.claude-code", other); err != nil { t.Fatalf("another secret's ask was held back: %v", err) } - if _, err := v.AskEnd(ControllerCaller, first.Call, "", "dismissed"); err != nil { + if _, err := v.AskEnd("g14.claude-code", first.Call, "", "dismissed"); err != nil { t.Fatal(err) } for i := 0; i < 2; i++ { - o, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + o, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")) if err != nil { t.Fatalf("ask %d of the hour: %v", i+2, err) } - if _, err := v.AskEnd(ControllerCaller, o.Call, "", "timed-out"); err != nil { + if _, err := v.AskEnd("g14.claude-code", o.Call, "", "timed-out"); err != nil { t.Fatal(err) } } - if _, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err == nil || + if _, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")); err == nil || !strings.Contains(err.Error(), "3 times in the last hour") { t.Fatalf("a fourth ask in the hour: %v", err) } @@ -165,13 +167,14 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) { } // An ask left open counts as open for SecretAskOpenFor only: a prompt that died does not hold the secret. w := newVault(t, nil) + w.AskCallers = stdio.Callers{"g14.claude-code"} clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC) w.Now = func() time.Time { return clock } - if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil { + if _, err := w.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")); err != nil { t.Fatal(err) } clock = clock.Add(SecretAskOpenFor + time.Second) - if _, err := w.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")); err != nil { + if _, err := w.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")); err != nil { t.Errorf("an ask left open past its time still held the secret: %v", err) } // 774 (ADR 0563 §8): a caller the manifest names asks directly, is named by its proven caller and never by the @@ -186,7 +189,7 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) { if err != nil || opened.AskedBy != "g14.node-tools" { t.Fatalf("a named caller's ask: %+v, %v", opened, err) } - if _, err := x.AskEnd(ControllerCaller, opened.Call, "", "dismissed"); err == nil { + if _, err := x.AskEnd("g14.claude-code", opened.Call, "", "dismissed"); err == nil { t.Error("another caller ended a named caller's ask") } if _, err := x.AskOpen("ace.node-tools", telegramAsk("")); err == nil || !strings.Contains(err.Error(), "still open (asked by g14.node-tools)") { @@ -199,10 +202,11 @@ func TestAnAskIsBoundedToOneOpenPerSecretAndThreeAnHour(t *testing.T) { }) } -// Only the controller reaches a desk's prompt, so only it opens and ends an ask; a value not sealed to the -// call's key, or a call that waited too long, keeps nothing. -func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) { +// Only a caller the vault's manifest names opens an ask, and only it ends it; a value not sealed to the call's key, or +// a call that waited too long, keeps nothing. +func TestAnAskIsTakenFromANamedCallerAloneAndOnlySealedToItsCall(t *testing.T) { v := newVault(t, nil) + v.AskCallers = stdio.Callers{"g14.claude-code"} for _, caller := range []string{"g14.node-tools", "unproven", "", "person.jochen"} { if _, err := v.AskOpen(caller, telegramAsk("x")); err == nil { t.Errorf("%q opened an ask", caller) @@ -211,7 +215,7 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) if asks, _ := v.Store.Asks(10); len(asks) != 0 { t.Fatalf("a refused caller filled the record: %+v", asks) } - opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + opened, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")) if err != nil { t.Fatal(err) } @@ -219,7 +223,7 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) t.Error("another caller ended the ask") } other, _, _ := box.GenerateKey(rand.Reader) - if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(other[:]), "x"), ""); err == nil || + if _, err := v.AskEnd("g14.claude-code", opened.Call, sealToCall(t, base64.StdEncoding.EncodeToString(other[:]), "x"), ""); err == nil || !strings.Contains(err.Error(), "not sealed to this call") { t.Errorf("a value sealed to another key: %v", err) } @@ -227,7 +231,7 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) t.Error("a value not sealed to the call was kept") } // The call's key is forgotten once used: the same call cannot be answered twice. - if _, err := v.AskEnd(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil { + if _, err := v.AskEnd("g14.claude-code", opened.Call, sealToCall(t, opened.SealTo, "x"), ""); err == nil { t.Error("a call was answered twice") } asks, _ := v.Store.Asks(1) @@ -237,18 +241,18 @@ func TestAnAskIsTakenFromTheControllerAloneAndOnlySealedToItsCall(t *testing.T) // A call that waited longer than its time is closed. clock := time.Date(2026, 10, 14, 9, 0, 0, 0, time.UTC) v.Now = func() time.Time { return clock } - late, err := v.IssuedOpen(ControllerCaller, "plex-api", "anchor", "plex") + late, err := v.IssuedOpen("g14.claude-code", "plex-api", "anchor", "plex") if err != nil { t.Fatal(err) } clock = clock.Add(CallOpenFor + time.Second) - if _, err := v.IssuedTake(ControllerCaller, late.Call, sealToCall(t, late.SealTo, "x")); err == nil { + if _, err := v.IssuedTake("g14.claude-code", late.Call, sealToCall(t, late.SealTo, "x")); err == nil { t.Error("a call that waited too long was taken") } // An empty answer gives nothing and says so. clock = clock.Add(time.Hour) - empty, _ := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) - if _, err := v.AskEnd(ControllerCaller, empty.Call, sealToCall(t, empty.SealTo, " \n"), ""); err == nil { + empty, _ := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")) + if _, err := v.AskEnd("g14.claude-code", empty.Call, sealToCall(t, empty.SealTo, " \n"), ""); err == nil { t.Error("an empty answer was kept") } if asks, _ := v.Store.Asks(1); asks[0].Outcome != "empty" { @@ -365,62 +369,14 @@ func TestAnIssuingProviderAnswersOnlyTheVaultsCallSealedToTheVault(t *testing.T) !strings.Contains(err.Error(), "software") { t.Errorf("rotate of an issued value: %v", err) } - if _, err := v.AcceptOpen(ControllerCaller, "provision/anchor/plex-api", "person.jochen", ""); err == nil { - t.Error("an issued value was taken by accept") - } if strings.Contains(everything(t, v), "plex-token") { t.Fatal("the issued value is in something the vault said or wrote in the clear") } } -// ---- accept and rotate ------------------------------------------------------------------------------------- - -// A value given at the controller's terminal is sealed there to the vault's call and opened here alone; an -// accepted provision's credential is never remade; a module's own given value rotates as a made one (ADR 0228). -func TestAnAcceptedValueOpensInTheVaultAloneAndIsNotRemade(t *testing.T) { - v := newVault(t, AllowList{"g14.node-provisioner": {"provision/novox/postgres", "own/g14/dunst/token"}}) - if _, err := v.AcceptOpen("g14.claude-code", "provision/novox/postgres", "", ""); err == nil { - t.Error("accept was opened for a caller that is not the controller") - } - for name, value := range map[string]string{"provision/novox/postgres": "pw-of-last-year", "own/g14/dunst/token": "tok"} { - opened, err := v.AcceptOpen(ControllerCaller, name, "jochen", "") - if err != nil { - t.Fatal(err) - } - changed, err := v.AcceptTake(ControllerCaller, opened.Call, sealToCall(t, opened.SealTo, value)) - if err != nil { - t.Fatal(err) - } - if changed.By != "jochen" { - t.Errorf("%s accepted by %q", name, changed.By) - } - if got := handedTo(t, v, "g14.node-provisioner", name); got != value { - t.Errorf("%s holds %q", name, got) - } - rec, _, _ := v.Store.Read(name) - if rec.Origin != OriginAccepted || rec.ChangedVia != ControllerCaller { - t.Errorf("%s: origin %q via %q", name, rec.Origin, rec.ChangedVia) - } - } - if _, err := v.Rotate(ControllerCaller, "provision/novox/postgres", "jochen", "leaked"); err == nil || - !strings.Contains(err.Error(), "not remade") { - t.Errorf("rotate of an accepted credential: %v", err) - } - rotated, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen", "leaked") - if err != nil || rotated.Generation != 2 { - t.Fatalf("rotate of a module's own given value: %+v %v", rotated, err) - } - if got := handedTo(t, v, "g14.node-provisioner", "own/g14/dunst/token"); got == "tok" || len(got) != 40 { - t.Errorf("rotated to %q", got) - } - if strings.Contains(everything(t, v), "pw-of-last-year") { - t.Fatal("an accepted value is in something the vault said or wrote in the clear") - } -} - // Rotate: refused for a secret not held and for an unproven caller; who asked is the proven caller, or the // person the controller names when it carries the act — both kept. -func TestRotateKeepsWhoAskedAndWhoCarriedIt(t *testing.T) { +func TestRotateKeepsTheProvenCallerAsWhoAsked(t *testing.T) { v := newVault(t, nil) if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen", "leaked"); err == nil { t.Error("a secret not held was rotated") @@ -431,11 +387,12 @@ func TestRotateKeepsWhoAskedAndWhoCarriedIt(t *testing.T) { if _, err := v.Rotate("unproven", "own/g14/dunst/token", "", "leaked"); err == nil { t.Error("an unproven caller rotated") } - if _, err := v.Rotate(ControllerCaller, "own/g14/dunst/token", "jochen", "leaked"); err != nil { + // A `by` is a word, never who asked: who asked is the caller the runtime proved (no caller relays another's, ADR 0590). + if _, err := v.Rotate("person.jochen", "own/g14/dunst/token", "someone else", "leaked"); err != nil { t.Fatal(err) } rec, _, _ := v.Store.Read("own/g14/dunst/token") - if rec.ChangedBy != "jochen" || rec.ChangedVia != ControllerCaller || rec.Generation != 2 { + if rec.ChangedBy != "person.jochen" || rec.ChangedVia != "" || rec.Generation != 2 { t.Errorf("by %q via %q generation %d", rec.ChangedBy, rec.ChangedVia, rec.Generation) } } @@ -491,6 +448,7 @@ func TestTheNamesOfWhatIsTakenIn(t *testing.T) { // Each verb takes its caller from the runtime, never from its arguments, and names its secret one way only. func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) { v := newVault(t, nil) + v.AskCallers = stdio.Callers{"g14.claude-code"} env := &Env{Vault: v.Vault} defs := definitions(env) var asked []map[string]any @@ -499,11 +457,11 @@ func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) { "plex.plex_issued_credential@anchor": standInProvider(&software)}}).ask ask := toolNamed(t, defs, Seat+".secret-ask") args := map[string]any{"node": "anchor", "module": "telegram", "name": "telegram-token", "desk": "laptop", - "asked_by": "g14.claude-code", "caller": ControllerCaller} + "caller": "ace.claude-code"} if _, err := ask.Run(args); err == nil { t.Error("an ask opened for a caller named in the arguments") } - answer, err := ask.runAs(ControllerCaller, args) + answer, err := ask.runAs("g14.claude-code", args) if err != nil || answer.(Asked).Outcome != "given" || len(asked) != 1 { t.Fatalf("secret-ask: %+v %v", answer, err) } @@ -512,18 +470,6 @@ func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) { t.Fatalf("secret_asks: %v %v", listed, err) } - accept := toolNamed(t, defs, Seat+".accept") - if _, err := accept.runAs(ControllerCaller, map[string]any{"node": "g14", "provision": "postgres"}); err == nil { - t.Error("accept took a name both ways") - } - answer, err = accept.runAs(ControllerCaller, map[string]any{"provider": "novox", "provision": "postgres", "by": "jochen"}) - if err != nil { - t.Fatal(err) - } - o := answer.(Opened) - if _, err := accept.runAs(ControllerCaller, map[string]any{"call": o.Call, "sealed": sealToCall(t, o.SealTo, "pw")}); err != nil { - t.Fatal(err) - } rotate := toolNamed(t, defs, Seat+".rotate") if _, err := rotate.runAs("g14.claude-code", map[string]any{"node": "g14", "module": "dunst", "name": "token"}); err == nil { t.Error("a secret not held was rotated through the verb") @@ -547,7 +493,8 @@ func TestTheVerbsTakeTheirCallerFromTheRuntime(t *testing.T) { // With the lock held elsewhere, AskEnd waits, and writes nothing until it is let go. func TestTheEndOfAnAskIsWrittenUnderTheRecordsLock(t *testing.T) { v := newVault(t, nil) - opened, err := v.AskOpen(ControllerCaller, telegramAsk("g14.claude-code")) + v.AskCallers = stdio.Callers{"g14.claude-code"} + opened, err := v.AskOpen("g14.claude-code", telegramAsk("g14.claude-code")) if err != nil { t.Fatal(err) } @@ -562,7 +509,7 @@ func TestTheEndOfAnAskIsWrittenUnderTheRecordsLock(t *testing.T) { v.Store.asksMu.Lock() done := make(chan error, 1) go func() { - _, err := v.AskEnd(ControllerCaller, opened.Call, "", "dismissed") + _, err := v.AskEnd("g14.claude-code", opened.Call, "", "dismissed") done <- err }() select { diff --git a/modules/mesh-vault/cmd/mesh-vault/issued_account_test.go b/modules/mesh-vault/cmd/mesh-vault/issued_account_test.go index 41f600e5..3d49ba92 100644 --- a/modules/mesh-vault/cmd/mesh-vault/issued_account_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/issued_account_test.go @@ -87,13 +87,11 @@ func eventsNamed(v testVault, from int, name string) int { // ADR 0374, issue 787): kept as that module's own secret on that machine, served to the controller's core get and to // the moved module's client, replaced in place on a re-issue with `secret-changed`, and never made anew by the vault. +// handIssued keeps a covered account as the controller's accept kept one with origin issued (accept went with the +// controller, ADR 0590): what the vault holds of it is served, hashed and never made anew. func handIssued(t *testing.T, v testVault, caller, secret, value string) (Changed, error) { t.Helper() - opened, err := v.AcceptOpen(caller, secret, "module issue", OriginIssued) - if err != nil { - return Changed{}, err - } - return v.AcceptTake(caller, opened.Call, sealToCall(t, opened.SealTo, value)) + return v.keepWith(secret, value, "module issue", caller, OriginIssued, keepOptions{refuse: v.vaultMinted}) } func TestAHandedBusAccountIsKeptServedReplacedAndNeverMadeByTheVault(t *testing.T) { @@ -142,19 +140,6 @@ func TestAHandedBusAccountIsKeptServedReplacedAndNeverMadeByTheVault(t *testing. t.Errorf("a refused rotation changed the value to %q", got) } - // A secret that is not a covered account is not handed with origin issued; and only the controller hands one. - if _, err := v.AcceptOpen(ControllerCaller, "own/g14/moved-bot/token", "x", OriginIssued); err == nil { - t.Error("origin issued was taken for a secret that is not a bus account") - } - if _, err := v.AcceptOpen(ControllerCaller, "provision/anchor/postgres", "x", OriginIssued); err == nil { - t.Error("origin issued was taken for a provision's credential") - } - if _, err := v.AcceptOpen("g14.moved-bot", moved, "x", OriginIssued); err == nil { - t.Error("a caller that is not the controller handed a covered account") - } - if _, err := v.AcceptOpen(ControllerCaller, moved, "x", "made"); err == nil { - t.Error("an origin accept does not take was taken") - } if strings.Contains(everything(t, v), "first-pw") || strings.Contains(everything(t, v), "second-pw") { t.Fatal("a handed account is in something the vault said or wrote in the clear") } diff --git a/modules/mesh-vault/cmd/mesh-vault/mint.go b/modules/mesh-vault/cmd/mesh-vault/mint.go index 414cf5bd..3b20a47e 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint.go @@ -372,9 +372,8 @@ func (v *Vault) IssueAccount(caller, user string) (any, error) { if user == "" { return nil, errors.New("issue-account names a bus user (`user`), e.g. ace.mesh-build-go") } - if nodeOrController(user) { - return nil, fmt.Errorf("%s is never minted by the vault: a node's account and the controller's keep the hash they "+ - "have (ADR 0561 §4)", user) + if nodeAccount(user) { + return nil, fmt.Errorf("%s is never minted by the vault: a node's account keeps the hash it has (ADR 0561 §4)", user) } raw, found, err := []byte(nil), false, error(nil) if v.WantRead != nil { @@ -417,7 +416,7 @@ func (v *Vault) wanted(user string) (buscompose.WantedAccount, bool) { return w, ok } -// nodeOrController says user is a node's account or the controller's: never minted here (ADR 0561 §4, §9). -func nodeOrController(user string) bool { - return user == ControllerCaller || strings.HasPrefix(user, "node.") +// nodeAccount says user is a node's account: never minted here (ADR 0561 §4, §9). +func nodeAccount(user string) bool { + return strings.HasPrefix(user, "node.") } diff --git a/modules/mesh-vault/cmd/mesh-vault/mint_test.go b/modules/mesh-vault/cmd/mesh-vault/mint_test.go index d1fc78b3..8ca556f6 100644 --- a/modules/mesh-vault/cmd/mesh-vault/mint_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/mint_test.go @@ -161,8 +161,7 @@ func TestIssueAccountMintsOneWantedUserNowAndRefusesTheRest(t *testing.T) { return nil, false, nil } for user, says := range map[string]string{ - "node.ace": "never minted", - "controller": "never minted", + "node.ace": "never minted", } { if _, err := v.IssueAccount("person.jochen", user); err == nil || !strings.Contains(err.Error(), says) { t.Errorf("%s: %v, want a refusal saying %q", user, err, says) diff --git a/modules/mesh-vault/cmd/mesh-vault/owncalls_test.go b/modules/mesh-vault/cmd/mesh-vault/owncalls_test.go index 5629bc5d..74352692 100644 --- a/modules/mesh-vault/cmd/mesh-vault/owncalls_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/owncalls_test.go @@ -10,6 +10,7 @@ import ( "encoding/base64" "encoding/json" "errors" + stdio "git.novox.be/novox/mesh-sdk/go" "strings" "sync" "testing" @@ -93,15 +94,16 @@ func aDesk(typed string, asked *[]map[string]any) func(string, map[string]any) ( // is typed opened here alone; what crossed is a key and a sealed box. func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T) { v := newVault(t, AllowList{"anchor.node-provisioner": {"own/anchor/telegram/telegram-token"}}) + v.AskCallers = stdio.Callers{"g14.claude-code"} var asked []map[string]any mesh := &aMesh{desk: aDesk(typedAtTheDesk, &asked)} v.Ask = mesh.ask - got, err := v.AskAtDesk(ControllerCaller, telegramAsk("g14/claude-code, via the console")) + got, err := v.AskAtDesk("g14.claude-code", telegramAsk("g14/claude-code, via the console")) if err != nil { t.Fatal(err) } - if got.Outcome != "given" || got.Generation != 1 || got.AskedBy != "g14/claude-code" || + if got.Outcome != "given" || got.Generation != 1 || got.AskedBy != "g14.claude-code" || got.Secret != "own/anchor/telegram/telegram-token" { t.Errorf("answered %+v", got) } @@ -109,7 +111,7 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T t.Fatalf("the vault asked %v", mesh.keys) } p := asked[0] - if p["asked_by"] != "g14/claude-code" || p["module"] != "telegram" || p["secret"] != "telegram-token" || + if p["asked_by"] != "g14.claude-code" || p["module"] != "telegram" || p["secret"] != "telegram-token" || p["node"] != "anchor" || p["seal_to"] == "" || p["timeout_seconds"] != float64(DeskPromptFor) { t.Errorf("the prompt was asked %v", p) } @@ -117,7 +119,7 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T t.Errorf("the vault holds %q", v) } rec, _, _ := v.Store.Read("own/anchor/telegram/telegram-token") - if rec.Origin != OriginTyped || rec.ChangedBy != "g14/claude-code" || rec.ChangedVia != ControllerCaller { + if rec.Origin != OriginTyped || rec.ChangedBy != "g14.claude-code" || rec.ChangedVia != "" { t.Errorf("the record: origin %q by %q via %q", rec.Origin, rec.ChangedBy, rec.ChangedVia) } asks, _ := v.Store.Asks(10) @@ -131,9 +133,10 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T // Dismissed and timed out end the ask so, and keep nothing. for typed, outcome := range map[string]string{"": "dismissed", "\x00timeout": "timed-out"} { w := newVault(t, nil) + w.AskCallers = stdio.Callers{"g14.claude-code"} var a []map[string]any w.Ask = (&aMesh{desk: aDesk(typed, &a)}).ask - got, err := w.AskAtDesk(ControllerCaller, telegramAsk("g14.claude-code")) + got, err := w.AskAtDesk("g14.claude-code", telegramAsk("g14.claude-code")) if err != nil || got.Outcome != outcome { t.Errorf("%s: %+v %v", outcome, got, err) } @@ -146,15 +149,17 @@ func TestTheVaultAsksTheDesksPromptItselfAndOnlyASealedValueCrosses(t *testing.T } // Answered empty: the operator's answer, said so, nothing kept. e := newVault(t, nil) + e.AskCallers = stdio.Callers{"g14.claude-code"} var ea []map[string]any e.Ask = (&aMesh{desk: aDesk(" ", &ea)}).ask - if got, err := e.AskAtDesk(ControllerCaller, telegramAsk("g14.claude-code")); err != nil || got.Outcome != "empty" { + if got, err := e.AskAtDesk("g14.claude-code", telegramAsk("g14.claude-code")); err != nil || got.Outcome != "empty" { t.Errorf("empty: %+v %v", got, err) } // The desk cannot be asked: refused, recorded so. r := newVault(t, nil) + r.AskCallers = stdio.Callers{"g14.claude-code"} r.Ask = (&aMesh{}).ask - if _, err := r.AskAtDesk(ControllerCaller, telegramAsk("g14.claude-code")); err == nil { + if _, err := r.AskAtDesk("g14.claude-code", telegramAsk("g14.claude-code")); err == nil { t.Error("an ask nobody answered was said to be done") } if asks, _ := r.Store.Asks(1); asks[0].Outcome != "refused" { diff --git a/modules/mesh-vault/cmd/mesh-vault/perholder_test.go b/modules/mesh-vault/cmd/mesh-vault/perholder_test.go index 440e8752..b739c84e 100644 --- a/modules/mesh-vault/cmd/mesh-vault/perholder_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/perholder_test.go @@ -153,13 +153,10 @@ func TestAProviderIssuesAValueForEachHolderAndEachHolderGetsOnlyItsOwn(t *testin moved.Holders[1].Generation != 2 { t.Errorf("renewed: %+v %v", moved, err) } - // A holder's value is issued by the software: never rotated, never accepted. + // A holder's value is issued by the software: never rotated. if _, err := v.Rotate(ControllerCaller, laptopsToken, "jochen", "test"); err == nil { t.Error("a holder's issued value was rotated") } - if _, err := v.AcceptOpen(ControllerCaller, laptopsToken, "jochen", ""); err == nil { - t.Error("a holder's issued value was opened for accept") - } } // A holder the tool was not asked about, a holder twice, a value not sealed to the call, or an answer of both diff --git a/modules/mesh-vault/cmd/mesh-vault/rotation.go b/modules/mesh-vault/cmd/mesh-vault/rotation.go index 4322e626..d69d71a3 100644 --- a/modules/mesh-vault/cmd/mesh-vault/rotation.go +++ b/modules/mesh-vault/cmd/mesh-vault/rotation.go @@ -35,11 +35,11 @@ func (v *Vault) LoadedListHeard(revision uint64) { } // BeginRotation records a rotation of user's account at the loaded list's revision, under the one rule: none for a -// node's account or the controller's, none while minting is off, none of an account the vault did not make or was not +// node's account, none while minting is off, none of an account the vault did not make or was not // handed, none twice for one account, and none when another began at this revision. func (v *Vault) BeginRotation(user string) (Rotating, error) { - if nodeOrController(user) { - return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account and the controller's keep the hash they have (ADR 0561 §4)", user) + if nodeAccount(user) { + return Rotating{}, fmt.Errorf("%s does not rotate here: a node's account keeps the hash it has (ADR 0561 §4)", user) } v.minting.mu.Lock() revision := v.minting.loadedRevision diff --git a/modules/mesh-vault/cmd/mesh-vault/sensitive.go b/modules/mesh-vault/cmd/mesh-vault/sensitive.go index 1bad865a..ef0d540b 100644 --- a/modules/mesh-vault/cmd/mesh-vault/sensitive.go +++ b/modules/mesh-vault/cmd/mesh-vault/sensitive.go @@ -62,7 +62,7 @@ func JudgeSensitive(read ActReader, now func() time.Time, caller, verb, act stri return fmt.Errorf("%s: the hand act %s was recorded by %s, not by %s who asks", verb, act, a.By, caller) case now().Sub(a.At) > HandActWithin: return fmt.Errorf("%s: the hand act %s is older than %s: record a new one", verb, act, HandActWithin) - case a.Verb != verb && !strings.HasSuffix(a.Verb, "."+verb): + case a.Verb != Seat+"."+verb: return fmt.Errorf("%s: the hand act %s is %q: it warrants no %s", verb, act, a.Verb, verb) } // mesh-hand-acts records one what; an act for several names lists them space-separated, so each word is a name. diff --git a/modules/mesh-vault/cmd/mesh-vault/sensitive_test.go b/modules/mesh-vault/cmd/mesh-vault/sensitive_test.go index 962dae66..e7b33417 100644 --- a/modules/mesh-vault/cmd/mesh-vault/sensitive_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/sensitive_test.go @@ -13,9 +13,12 @@ func TestEachSensitiveVerbJudgesItsOwnCaller(t *testing.T) { now := time.Date(2026, 11, 2, 12, 0, 0, 0, time.UTC) acts := map[string]HandActSeen{ "mine": {ID: "mine", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}}, - "old": {ID: "old", By: "novox.mesh-healers", At: now.Add(-25 * time.Hour), Verb: "rotate", Args: []string{"own/g14/app/key"}}, - "other": {ID: "other", By: "novox.mesh-config", At: now.Add(-time.Hour), Verb: "rotate", Args: []string{"own/g14/app/key"}}, - "del": {ID: "del", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "provisioner_delete", Args: []string{"d-app"}}, + "old": {ID: "old", By: "novox.mesh-healers", At: now.Add(-25 * time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}}, + "other": {ID: "other", By: "novox.mesh-config", At: now.Add(-time.Hour), Verb: "mesh-vault.rotate", Args: []string{"own/g14/app/key"}}, + "del": {ID: "del", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "mesh-vault.provisioner_delete", Args: []string{"d-app"}}, + // Review 1: a verb of another seat or module that ends in the vault verb's name warrants nothing. + "alike": {ID: "alike", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "other-module.rotate", Args: []string{"own/g14/app/key"}}, + "bare": {ID: "bare", By: "novox.mesh-healers", At: now.Add(-time.Hour), Verb: "rotate", Args: []string{"own/g14/app/key"}}, // As mesh-hand-acts records nox's act: one what, the consumers space-separated (review 1 of issue 1126). "two": {ID: "two", By: "shanks.node-tools", At: now.Add(-time.Minute), Verb: "mesh-vault.provisioner_retire_approve", Args: []string{"c1 c2"}}, } @@ -43,6 +46,8 @@ func TestEachSensitiveVerbJudgesItsOwnCaller(t *testing.T) { {"an act too old", "novox.mesh-healers", SensitiveRotate, "old", []string{"own/g14/app/key"}, "older than"}, {"an act not recorded", "novox.mesh-healers", SensitiveDelete, "none", []string{"d-app"}, "holds no hand act"}, {"a delete's own act", "novox.mesh-healers", SensitiveDelete, "del", []string{"d-app"}, ""}, + {"an act of another seat's rotate", "novox.mesh-healers", SensitiveRotate, "alike", []string{"own/g14/app/key"}, "warrants no rotate"}, + {"an act naming the verb without its seat", "novox.mesh-healers", SensitiveRotate, "bare", []string{"own/g14/app/key"}, "warrants no rotate"}, {"one act for two consumers", "shanks.node-tools", SensitiveRetireApprove, "two", []string{"c1", "c2"}, ""}, {"one act naming two, asked for a third", "shanks.node-tools", SensitiveRetireApprove, "two", []string{"c1", "c3"}, "not c3"}, } { diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index ad9bb635..16dd07c5 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -93,36 +93,9 @@ func definitions(env *Env) []toolDef { key, _ := a["public_key"].(string) return env.Vault.Get(caller, strings.TrimSpace(name), key) }}, - {Name: Seat + ".accept", - Description: "Take a value given at the controller's terminal (ADR 0349 clause 8), in two steps from the " + - "controller alone: name the secret (node, module and name for a module's own; provider and provision " + - "for a provision's one credential) and get a call and a key made for it; then send the call and the " + - "value sealed to that key. The vault alone opens it, and never remakes an accepted credential. With " + - "origin issued it keeps a covered bus account the controller minted, replacing it in place.", - Input: intakeInput(map[string]string{"node": "the machine, for a module's own secret", - "module": "the module, for a module's own secret", "name": "the secret's name in the module", - "provider": "the provider's machine, for a provision's credential", "provision": "the provision", - "by": "who gave it at the terminal, as the controller names them", - "origin": "issued for a bus account the controller minted for a core or moved module and hands at " + - "once (ADR 0374, issue 787), kept as that module's own secret and replaced in place on a " + - "re-issue; left out for a value given at the terminal"}), - run: func(caller string, a map[string]any) (any, error) { - v, err := env.ready() - if err != nil { - return nil, err - } - if call := arg(a, "call"); call != "" { - return v.AcceptTake(caller, call, arg(a, "sealed")) - } - secret, err := secretNamed(a) - if err != nil { - return nil, err - } - return v.AcceptOpen(caller, secret, arg(a, "by"), arg(a, "origin")) - }}, {Name: Seat + ".secret-ask", Description: "Ask the operator for a module's own secret at a desk (ADR 0349 clause 9, ADR 0277 §22–§27), " + - "asked by the controller (naming asked_by) or by a caller the vault's manifest names, itself named by the " + + "asked by a caller the vault's manifest names, itself named by the " + "bus (ADR 0563 §8): node, module, name and desk. The ask is recorded before any " + "prompt opens — one open ask per secret and three an hour — and the vault asks the desk's prompt itself, " + "in its own name, with a key made for the call; what is typed opens in the vault alone. Answers how the " + @@ -130,15 +103,14 @@ func definitions(env *Env) []toolDef { "the value.", Input: map[string]any{"type": "object", "properties": map[string]any{ "node": str("the machine the secret is for"), "module": str("the module"), - "name": str("the secret's name in the module"), "desk": str("the machine whose prompt asks"), - "asked_by": str("from the controller alone: who asked it, as the bus named its caller")}}, + "name": str("the secret's name in the module"), "desk": str("the machine whose prompt asks")}}, run: func(caller string, a map[string]any) (any, error) { v, err := env.ready() if err != nil { return nil, err } return v.AskAtDesk(caller, AskOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"), - Desk: arg(a, "desk"), AskedBy: arg(a, "asked_by")}) + Desk: arg(a, "desk")}) }}, {Name: Seat + ".issued", Description: "Take the credential a provider's software issued for a provision (ADR 0349 clause 10, ADR 0305), " + diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 531e8349..7cd0b706 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -370,15 +370,8 @@ func (v *Vault) Get(caller, name, publicKey string) (Answer, error) { _ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why}) return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why) } - // The core's own credentials, to the controller alone, sealed only to their machine's key (ADR 0349 §16). if isLogin { // Judged above: the node's own engine, sealed to its own key. - } else if caller == ControllerCaller { - if why := v.coreRefusal(name, publicKey); why != "" { - v.log("refused %s to %s: %s", name, caller, why) - _ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why}) - return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why) - } } else if ok, why := askers.May(caller, name); !ok { if reason, notYet := strings.CutPrefix(why, notYetPrefix); notYet { // Not a refusal (issue 928): the client waits and asks again, and nothing is recorded against it. diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index 964ecdbd..72758138 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -198,7 +198,6 @@ "scope": "mesh", "serves": [ "get", - "accept", "secret-ask", "issued", "rotate",