Open a value only in the vault, sealed to a key made for its call (issue 675)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 19:12:21 +00:00
co-authored by Claude Opus 5.5
parent eba300794f
commit efddb634e5
+472
View File
@@ -0,0 +1,472 @@
package main
// What the vault takes in, and how it rotates (novox/hq ADR 0349 clauses 8 to 10; ADR 0277 §22–§27 and ADR 0305
// with the vault in the controller's place; design 57's V2, issue 675).
//
// Four verbs of the seat: `accept` (a value given at the controller's terminal), `secret-ask` (a value the
// operator types at a desk), `issued` (a value a provider's software issued) and `rotate`.
//
// **A value comes in sealed to a key the vault made for that one call, and opens only here.** The first three
// are two steps of one verb. The first names what is taken and answers a call and the public half of a key made
// for it (`seal_to`, a raw X25519 key in base64, the NaCl anonymous box the desk's prompt and a provider's tool
// already seal to); the second hands back what was sealed to that key, and the vault opens it with the private
// half, which never leaves this process and is forgotten once used or after two minutes.
//
// **The controller carries the call, and cannot open what it carries.** The desk's prompt and a provider's tool
// answer the controller alone: the bus lets only the controller ask them, on the subject that names its caller
// (issue 365). The vault cannot call in a name of its own while it runs in its machine's tool runner, whose one
// bus account carries that machine's agents' calls too (issue 569, ADR 0305 §3). So the controller asks them on
// the vault's behalf with the vault's key, and passes on the sealed answer: the value is opened in the vault and
// nowhere else (ADR 0349 clause 9, ADR 0277 §23), and the vault takes these steps from the controller alone.
//
// Which secret each one is, by name in the vault's store:
//
// own/<node>/<module>/<name> a module's own secret on one machine
// provision/<provider node>/<provision> the one credential of a provider assignment and provision (clause 8)
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"golang.org/x/crypto/nacl/box"
)
// ControllerCaller is the controller's bus user: the one caller that carries a value in (broker.ControllerName).
const ControllerCaller = "controller"
// Where a value came from.
const (
OriginMade = "made"
OriginAccepted = "accepted"
OriginTyped = "typed"
OriginIssued = "issued"
)
// CallOpenFor is how long a call's key waits for its sealed value: longer than any prompt or tool waits.
const CallOpenFor = 2 * time.Minute
// The bounds of asking for one secret (ADR 0277 §24, the controller's inventory.SecretAsk* until now).
const (
// SecretAskOpenFor is how long an ask that has not ended counts as open.
SecretAskOpenFor = 2 * time.Minute
// SecretAsksPerHour is how many asks for one secret an hour takes.
SecretAsksPerHour = 3
)
var namePart = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,62}$`)
// OwnSecret is the vault's name for a module's own secret on one machine.
func OwnSecret(node, module, name string) (string, error) {
for what, v := range map[string]string{"the machine": node, "the module": module, "the secret": name} {
if !namePart.MatchString(v) {
return "", fmt.Errorf("%s %q is not a name the mesh writes", what, v)
}
}
return "own/" + node + "/" + module + "/" + name, nil
}
// ProvisionSecret is the vault's name for the one credential of a provider assignment and provision.
func ProvisionSecret(provider, provision string) (string, error) {
for what, v := range map[string]string{"the provider's machine": provider, "the provision": provision} {
if !namePart.MatchString(v) {
return "", fmt.Errorf("%s %q is not a name the mesh writes", what, v)
}
}
return "provision/" + provider + "/" + provision, nil
}
// callKey is one call's key and what the call takes in. Never written anywhere.
type callKey struct {
verb, secret, by string
// ask is the ask's record for `secret-ask`, which the call's id is.
ask *AskRecord
public [32]byte
private [32]byte
until time.Time
}
// Opened is a first step's answer: the call, and the key to seal its value to. Never a value.
type Opened struct {
Call string `json:"call"`
SealTo string `json:"seal_to"`
Secret string `json:"secret"`
AskedBy string `json:"asked_by,omitempty"`
}
// fromTheController refuses every caller but the controller: only it reaches a desk's prompt and a provider's
// tool, 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
}
// openCall makes a key for one call and keeps its private half here until the call is taken or two minutes
// have passed.
func (v *Vault) openCall(verb, secret, by string, ask *AskRecord) (string, string, error) {
pub, priv, err := box.GenerateKey(rand.Reader)
if err != nil {
return "", "", err
}
idRaw := make([]byte, 12)
if _, err := rand.Read(idRaw); err != nil {
return "", "", err
}
id := hex.EncodeToString(idRaw)
if ask != nil {
id = ask.ID
}
v.callsMu.Lock()
defer v.callsMu.Unlock()
if v.pending == nil {
v.pending = map[string]*callKey{}
}
now := v.Now()
for k, c := range v.pending {
if now.After(c.until) {
delete(v.pending, k)
}
}
v.pending[id] = &callKey{verb: verb, secret: secret, by: by, ask: ask, public: *pub, private: *priv,
until: now.Add(CallOpenFor)}
return id, base64.StdEncoding.EncodeToString(pub[:]), nil
}
// takeCall ends a call: its key is forgotten whatever follows.
func (v *Vault) takeCall(verb, id string) (*callKey, error) {
v.callsMu.Lock()
defer v.callsMu.Unlock()
c, ok := v.pending[id]
if !ok || c.verb != verb {
return nil, fmt.Errorf("no %s call %q is open in the vault: it ended, it was never opened, or the vault "+
"started again; open it anew", verb, id)
}
delete(v.pending, id)
if v.Now().After(c.until) {
return nil, fmt.Errorf("the %s call %q waited longer than %s and is closed; open it anew", verb, id, CallOpenFor)
}
return c, nil
}
// open opens what was sealed to the call's key, and forgets the key.
func (c *callKey) open(sealed string) ([]byte, error) {
defer func() { c.private = [32]byte{} }()
blob, err := base64.StdEncoding.DecodeString(strings.TrimSpace(sealed))
if err != nil {
return nil, errors.New("the value is not sealed to this call; nothing was taken")
}
out, ok := box.OpenAnonymous(nil, blob, &c.public, &c.private)
if !ok {
// Never the value, never what failed to open: only that it was not sealed to this call.
return nil, errors.New("the value is not sealed to this call; nothing was taken")
}
return out, nil
}
func zero(b []byte) {
for i := range b {
b[i] = 0
}
}
// ---- accept -------------------------------------------------------------------------------------------------
// 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).
func (v *Vault) AcceptOpen(caller, secret, by string) (Opened, error) {
if err := fromTheController(caller, "accept"); err != nil {
return Opened{}, err
}
if err := validName(secret); err != nil {
return Opened{}, err
}
if rec, held, err := v.Store.Read(secret); err != nil {
return Opened{}, err
} else if held && rec.Origin == OriginIssued {
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)
}
id, key, err := v.openCall("accept", secret, by, nil)
if err != nil {
return Opened{}, err
}
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")
}
return v.keep(c.secret, string(value), someoneOr(c.by, caller), caller, OriginAccepted)
}
// ---- secret-ask ---------------------------------------------------------------------------------------------
// 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.
AskedBy string
}
// AskOpen records an ask for a module's own secret before any prompt opens, holding its bounds — one open ask
// per secret, three an hour (ADR 0277 §24, §25) — and answers the key the desk's prompt seals what is typed to,
// and who asked as the record says it, which the prompt names (§26).
func (v *Vault) AskOpen(caller string, a AskOf) (Opened, error) {
if err := fromTheController(caller, "secret-ask"); err != nil {
return Opened{}, err
}
secret, err := OwnSecret(a.Node, a.Module, a.Name)
if err != nil {
return Opened{}, fmt.Errorf("nobody was asked to type anything: %w", err)
}
if !namePart.MatchString(a.Desk) {
return Opened{}, fmt.Errorf("nobody was asked to type anything: the desk %q is not a machine's name", a.Desk)
}
askedBy := askedByName(a.AskedBy)
v.Store.asksMu.Lock()
defer v.Store.asksMu.Unlock()
now := v.Now().UTC()
recent, err := v.Store.AsksSince(now.Add(-time.Hour))
if err != nil {
return Opened{}, fmt.Errorf("nobody was asked to type anything: the record of asks cannot be read: %w", err)
}
ended := map[string]bool{}
for _, r := range recent {
if r.Event == AskEnded {
ended[r.ID] = true
}
}
lastHour, openBy := 0, ""
for _, r := range recent {
if r.Event != AskOpened || r.Secret != secret {
continue
}
lastHour++
if !ended[r.ID] && r.At.After(now.Add(-SecretAskOpenFor)) {
openBy = r.AskedBy
}
}
if openBy != "" {
return Opened{}, fmt.Errorf("an ask for %s of %s on %s is still open (asked by %s): one prompt at a time for a "+
"secret, and this one ends within %s", a.Name, a.Module, a.Node, openBy, SecretAskOpenFor)
}
if lastHour >= SecretAsksPerHour {
return Opened{}, fmt.Errorf("%s of %s on %s was asked for %d times in the last hour, and an hour takes %d asks "+
"for one secret: the operator is not kept at a prompt", a.Name, a.Module, a.Node, lastHour, SecretAsksPerHour)
}
idRaw := make([]byte, 12)
if _, err := rand.Read(idRaw); err != nil {
return Opened{}, err
}
rec := AskRecord{ID: hex.EncodeToString(idRaw), At: now, Event: AskOpened, Secret: secret, Node: a.Node,
Module: a.Module, Name: a.Name, Desk: a.Desk, AskedBy: askedBy, Caller: caller}
if err := v.recordAsk(rec); err != nil {
return Opened{}, fmt.Errorf("nobody was asked to type anything: the ask could not be recorded: %w", err)
}
id, key, err := v.openCall("secret-ask", secret, askedBy, &rec)
if err != nil {
_ = v.recordAsk(AskRecord{ID: rec.ID, At: v.Now().UTC(), Event: AskEnded, Secret: secret, Outcome: "failed"})
return Opened{}, err
}
return Opened{Call: id, SealTo: key, Secret: secret, AskedBy: askedBy}, nil
}
// The outcomes an ask ends with, as the controller's record had them.
var askOutcomes = map[string]bool{"dismissed": true, "timed-out": true, "empty": true, "refused": true, "failed": true}
// AskEnd ends an ask: with what the desk sealed to its key, which the vault opens and keeps as typed, or with
// the outcome the desk answered instead. How it ended is recorded whatever happens.
func (v *Vault) AskEnd(caller, call, sealed, outcome string) (Changed, error) {
if err := fromTheController(caller, "secret-ask"); err != nil {
return Changed{}, err
}
if sealed == "" && !askOutcomes[outcome] {
return Changed{}, fmt.Errorf("an ask ends with what was sealed to its key, or with one of dismissed, " +
"timed-out, empty, refused or failed")
}
c, err := v.takeCall("secret-ask", call)
if err != nil {
return Changed{}, err
}
end := func(how string) {
if err := v.recordAsk(AskRecord{ID: c.ask.ID, At: v.Now().UTC(), Event: AskEnded, Secret: c.secret,
Outcome: how}); err != nil {
v.log("the end of ask %s (%s) could not be recorded: %v", c.ask.ID, how, err)
}
}
if sealed == "" {
end(outcome)
return Changed{Name: c.secret, Unchanged: true}, nil
}
opened, err := c.open(sealed)
if err != nil {
end("failed")
return Changed{}, err
}
value := strings.TrimRight(string(opened), "\r\n")
zero(opened)
if strings.TrimSpace(value) == "" {
end("empty")
return Changed{}, fmt.Errorf("nothing was given: the prompt on %s was answered empty", c.ask.Desk)
}
changed, err := v.keep(c.secret, value, c.ask.AskedBy, caller, OriginTyped)
value = ""
if err != nil {
end("failed")
return Changed{}, err
}
end("given")
return changed, nil
}
func (v *Vault) recordAsk(r AskRecord) error {
rolled, err := v.Store.RecordAsk(r)
if rolled {
v.log("THE RECORD OF ASKS WAS ROLLED: it reached its bound of %d bytes; the oldest of %d kept files is "+
"dropped. Many asks in little time is worth a look: secret_asks", v.Store.HandedBound(), HandedKept)
}
return err
}
// askedByName is who asked as the prompt and the record name it: the first clause of what the bus said, in the
// characters a name has, at most 80 (the controller's own rule, ADR 0277 §26), so no words of the caller's own
// reach the prompt.
func askedByName(caller string) string {
first, _, _ := strings.Cut(caller, ",")
var b strings.Builder
for _, r := range strings.TrimSpace(first) {
if b.Len() >= 80 {
break
}
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '_', r == '/', r == '@',
r == '-', r == ' ':
b.WriteRune(r)
default:
b.WriteRune('-')
}
}
name := strings.TrimSpace(b.String())
if name == "" || strings.HasPrefix(name, "-") {
return "an unnamed caller"
}
return name
}
// ---- issued -------------------------------------------------------------------------------------------------
// IssuedOpen opens a call to take the credential a provider's software issued for a provision (ADR 0349 clause
// 10, ADR 0305 with the vault in the controller's place): the controller asks the provider's tool, on the
// provider's machine, with the call's key, and passes on what it answered.
func (v *Vault) IssuedOpen(caller, provision, provider, module string) (Opened, error) {
if err := fromTheController(caller, "issued"); err != nil {
return Opened{}, err
}
secret, err := ProvisionSecret(provider, provision)
if err != nil {
return Opened{}, err
}
if !namePart.MatchString(module) {
return Opened{}, fmt.Errorf("the provider's module %q is not a name the mesh writes", module)
}
id, key, err := v.openCall("issued", secret, module+" on "+provider, nil)
if err != nil {
return Opened{}, err
}
return Opened{Call: id, SealTo: key, Secret: secret}, nil
}
// IssuedTake opens what the provider's tool sealed to the call's key and keeps it as the provision's one
// credential. The same value again writes nothing (ADR 0305 §5): every holder that has it is left as it is.
func (v *Vault) IssuedTake(caller, call, sealed string) (Changed, error) {
if err := fromTheController(caller, "issued"); err != nil {
return Changed{}, err
}
c, err := v.takeCall("issued", call)
if err != nil {
return Changed{}, err
}
opened, err := c.open(sealed)
if err != nil {
return Changed{}, err
}
value := strings.TrimSpace(string(opened))
zero(opened)
if value == "" {
return Changed{}, fmt.Errorf("%s answered an empty value; nothing was taken", c.by)
}
return v.keep(c.secret, value, c.by, caller, OriginIssued)
}
// ---- rotate -------------------------------------------------------------------------------------------------
// Rotate makes a secret anew (ADR 0114, ADR 0228). Refused: a secret the vault does not hold; a value a
// provider's software issued, which the software rotates (ADR 0305 §9); and a provision's accepted credential,
// which the vault does not remake (ADR 0349 clause 8, ADR 0158 §8). A module's own value given by hand is
// rotated like a made one (ADR 0228). Who asked is the caller the runtime proved, or the person the controller
// names when the controller carries the act — both are kept.
func (v *Vault) Rotate(caller, secret, by string) (Changed, error) {
if caller == "" || caller == stdio.Unproven || !busUser.MatchString(caller) {
return Changed{}, errors.New("the vault rotates only for a caller the runtime proved")
}
if err := validName(secret); err != nil {
return Changed{}, err
}
rec, held, err := v.Store.Read(secret)
if err != nil {
return Changed{}, err
}
switch {
case !held:
return Changed{}, fmt.Errorf("not rotated: the vault holds no secret named %s", secret)
case rec.Origin == OriginIssued:
return Changed{}, fmt.Errorf("not rotated: %s is issued by its provider's software, which the mesh cannot "+
"make or rotate (ADR 0305 §9): change it in that software, and `secret issued` takes the value it holds now",
secret)
case rec.Origin == OriginAccepted && strings.HasPrefix(secret, "provision/"):
return Changed{}, fmt.Errorf("not rotated: %s was accepted, and an accepted credential is not remade by the "+
"vault (ADR 0349 clause 8): accept its new value instead", secret)
}
if caller != ControllerCaller || by == "" {
by = caller
}
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return Changed{}, err
}
return v.keep(secret, base64.RawURLEncoding.EncodeToString(raw), by, caller, OriginMade)
}
func someoneOr(by, caller string) string {
if strings.TrimSpace(by) == "" {
return caller
}
return by
}