Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/sensitive.go
T
jschoubben ecc7621274 Privilege no caller named controller in the vault; match a hand act's verb exactly
The controller is not placed (ADR 0590), yet the vault still answered a
caller named controller alone (accept), took its relay of who asked
(secret-ask, rotate's by) and sealed it the core's credentials. Those
branches and accept go with their tests; a node's account alone is never
minted or rotated here. The sensitive-verb judge now needs the act's verb to
be the vault's own, mesh-vault.<verb>, not any verb ending so (issue 1126
review 1).

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 12:29:56 +01:00

95 lines
4.0 KiB
Go

package main
// **Each sensitive verb judges its own caller** (novox/hq ADR 0590 §8, issue 1126). The controller judged the
// operator's account for these verbs (ADR 0272 §6, §9) and relayed who asked at its terminal; it is not placed, and
// nox and every local user reach the vault through the mesh MCP server on their machine. So each verb that approves or
// deletes a retirement, rotates a secret or exports sealed copies answers only:
//
// - the operator's account: a caller the runtime proves as `person.<name>`; or
// - a recorded hand act: any other proven caller names (`act`) the hand act it recorded through mesh-hand-acts for
// this verb, read back here by its id: recorded by that same caller, within HandActWithin, for this verb, and
// naming what it acts on when the verb acts on something named (several names in one act's what, space-separated).
//
// Nothing else passes: an unproven caller, a module with no act, an act of another caller, verb or subject. A `by`
// argument is a word, never the proof (actOf keeps it beside the proven caller).
import (
"fmt"
"slices"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// PersonPrefix begins the bus user of a person's account: `person.<name>`.
const PersonPrefix = "person."
// The sensitive verbs, as a hand act names them.
const (
SensitiveRotate = "rotate"
SensitiveRetireApprove = "provisioner_retire_approve"
SensitiveDelete = "provisioner_delete"
SensitiveExport = "secret_export"
SensitiveVaultExport = "secret_vault_export"
)
// JudgeSensitive answers whether caller may do verb on about (the names it acts on, each of which a hand act must name;
// none for an export): nil for the operator's account, or for a hand act of caller's own that warrants it.
func JudgeSensitive(read ActReader, now func() time.Time, caller, verb, act string, about ...string) error {
caller = strings.TrimSpace(caller)
if caller == "" || caller == stdio.Unproven {
return fmt.Errorf("%s answers a caller the runtime proves, and it proved none (novox/hq ADR 0590 §8)", verb)
}
if strings.HasPrefix(caller, PersonPrefix) && len(caller) > len(PersonPrefix) {
return nil
}
if strings.TrimSpace(act) == "" {
return fmt.Errorf("%s answers the operator's account (person.<name>), or a caller naming the hand act it "+
"recorded for it through mesh-hand-acts (`act`); %s named none (novox/hq ADR 0590 §8)", verb, caller)
}
if read == nil {
return fmt.Errorf("%s: the vault reads no hand acts, so it answers only the operator's account", verb)
}
a, err := read(act)
if err != nil {
return fmt.Errorf("%s: the hand act %s could not be read back from mesh-hand-acts: %w", verb, act, err)
}
switch {
case a == nil:
return fmt.Errorf("%s: mesh-hand-acts holds no hand act %s: record one first", verb, act)
case a.By != caller:
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 != 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.
var named []string
for _, arg := range a.Args {
named = append(named, strings.Fields(arg)...)
}
for _, name := range about {
if !slices.Contains(named, name) {
return fmt.Errorf("%s: the hand act %s names %s, not %s", verb, act, strings.Join(a.Args, " "), name)
}
}
return nil
}
// judge is JudgeSensitive with the vault's reader of hand acts and its clock; with no vault ready, only the operator's
// account passes.
func (e *Env) judge(caller, verb string, a map[string]any, about ...string) error {
var read ActReader
now := time.Now
if e != nil && e.Vault != nil {
read = e.Vault.ReadAct
if e.Vault.Now != nil {
now = e.Vault.Now
}
}
act, _ := a["act"].(string)
return JudgeSensitive(read, now, caller, verb, act, about...)
}