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.** Each call opens with // 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) and ends with what was sealed to that key, which the vault opens // with the private half: it never leaves this process and is forgotten once used or after two minutes. // // **The vault asks the desk and the provider itself** (issue 723). The desk's prompt and a provider's issuing tool // are asked by the vault, in its own name, under the bus account its own runtime runs on (ADR 0259 §8's runs-as): // the bus lets that account alone call them naming the vault, so the prompt and the tool know the vault asks, and // what they seal to the call's key opens here and nowhere else (ADR 0349 clauses 9 and 10, ADR 0277 §23). The // controller asks `secret-ask` and `issued` once, carrying no key and no sealed value; it reaches the desk and the // tool itself only for a module still filled the old way, until R1 (issue 682). Only `accept` stays two steps: // the value is given at the controller's terminal, which seals it there to the call's key. // // Which secret each one is, by name in the vault's store: // // own/// a module's own secret on one machine // provision// the one credential of a provider assignment and provision (clause 8) import ( "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "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 // move is what `move-in` takes: the row of the controller's store (move.go). move *MoveOf // why is what `move-in` with replace_running was asked for, in the caller's words (owntake.go). why string 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"` } // 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 { if strings.TrimSpace(caller) == "" || caller == stdio.Unproven { return fmt.Errorf("%s answers a caller the runtime proves, and it proved none", verb) } 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 } // callKind is the kind of the open call id, or "" when none is open: what its second step is for. func (v *Vault) callKind(id string) string { v.callsMu.Lock() defer v.callsMu.Unlock() if c, ok := v.pending[id]; ok { return c.verb } return "" } // 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 } // openedBy is the caller that opened a secret-ask's call still open, as the ask's record names it; "" for none. func (v *Vault) openedBy(id string) string { v.callsMu.Lock() defer v.callsMu.Unlock() if c, ok := v.pending[id]; ok && c.verb == "secret-ask" && c.ask != nil { return c.ask.Caller } return "" } // 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{} }() return c.openOne(sealed) } // openOne opens one value sealed to the call's key and keeps the key, for a call that takes several (one per // holder, perholder.go); the caller forgets the key. func (c *callKey) openOne(sealed string) ([]byte, error) { 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 ------------------------------------------------------------------------------------------------- // CoveredSecrets are the names, in a module's own secrets, of the bus accounts the controller mints for a core module // or a module marked moved and hands to the vault (novox/hq ADR 0374, issue 787): the module's account on the bus // (`broker`), for the machine's provisioner client its own (`provisioner-broker`), and for each process of its bus // declarations its own (`broker-`, ADR 0436 §2), named here by ProcessSecretPrefix. var CoveredSecrets = []string{"broker", "provisioner-broker", ProcessSecretPrefix + ""} // ProcessSecretPrefix starts a process's own bus account's secret, `broker-` (the controller's // processSecretName). const ProcessSecretPrefix = "broker-" // Covered says whether secret is a covered account: a module's own secret named `broker`, `provisioner-broker` or // `broker-`. The vault cannot tell core or moved from a module's manifest; the controller, which can, is the // only caller that may hand one in, and the name keeps what it hands to these. func Covered(secret string) bool { if _, _, ok := ledgerAccountOf(secret); ok { return true } parts := strings.Split(secret, "/") if len(parts) != 4 || parts[0] != "own" { return false } name := parts[3] if id, process := strings.CutPrefix(name, ProcessSecretPrefix); process { return id != "" } return name == "broker" || name == "provisioner-broker" } // NamedAccount says secret is a bus account by its name alone: a module's own `broker` or a provisioner client's // `provisioner-broker`. A `broker-` is one only when a process of that id is declared with a bus, which its name // cannot say (mesh-controller's broker-address is the bus's address, no account): Vault.DeclaredAccount. func NamedAccount(secret string) bool { parts := strings.Split(secret, "/") return len(parts) == 4 && parts[0] == "own" && (parts[3] == "broker" || parts[3] == "provisioner-broker") } // 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 { if before.Origin != OriginMade || before.ChangedBy != "the vault" || before.MovedFrom != nil { return "" } if was, err := openAtRest(before, v); err == nil && BusAccountValue(before.Name, was) { return before.Name + " holds a bus account the vault minted (generation " + fmt.Sprint(before.Generation) + "): a value the controller issued never replaces it, so nothing was taken; the vault is the one issuer " + "(issue 774), and a new value is the vault's rotation" } return "" } // ---- secret-ask --------------------------------------------------------------------------------------------- // AskOf is what an ask is for, and where. type AskOf struct { Node, Module, Name, Desk string } // 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) { if v.AskCallers.Allows(caller) { return askedByName(caller), nil } if caller == "" { caller = stdio.Unproven } 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 // 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) { askedBy, err := v.askedByFor(caller, a) if 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) } 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 := v.askedByFor(caller, AskOf{}); err != nil { return Changed{}, err } // An ask is ended by the caller that opened it, and by no other. if opener := v.openedBy(call); opener != "" && opener != caller { return Changed{}, fmt.Errorf("the ask %s was opened by %s, and %s may not end it", call, opener, caller) } 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) { v.Store.asksMu.Lock() defer v.Store.asksMu.Unlock() 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("%w: the prompt on %s was answered empty", ErrNothingGiven, 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 } // recordAsk appends one line to the record of asks. The caller holds Store.asksMu, so no two writers roll the // record at once and no ask is counted against its bounds while another line is half written. 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 := fromAGrantedCaller(caller, "issued"); err != nil { return Opened{}, err } return v.openIssued(provision, provider, module) } // openIssued is IssuedOpen for a call the vault makes in its own name (ownIssue, issue 928). func (v *Vault) openIssued(provision, provider, module string) (Opened, error) { 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 := fromAGrantedCaller(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) } // ---- the vault's own calls (issue 723) ----------------------------------------------------------------------- // DeskPromptFor is how long the desk's prompt waits for the operator when the vault asks: under the thirty seconds // a call through the runtime waits, and under the controller's wait for `secret-ask`. const DeskPromptFor = 20 // DeskPrompt is the desk's secret prompt as the vault asks it: the seat's verb on the desk's machine. func DeskPrompt(desk string) string { return "seat:node-launcher.secret@" + desk } // Asked is how an ask at a desk ended: what was kept, or why nothing was. Never the value. type Asked struct { Secret string `json:"secret"` Generation int `json:"generation,omitempty"` AskedBy string `json:"asked_by"` // Outcome is given, dismissed, timed-out, empty, refused or failed. Outcome string `json:"outcome"` Unchanged bool `json:"unchanged,omitempty"` } // AskAtDesk is `secret-ask` (ADR 0349 clause 9, ADR 0277 §22–§27): asked by the controller, which names who asked it, // or by a caller the vault's manifest names, named by the runtime's proof (ADR 0563 §8). The ask is recorded and its bounds held before any prompt opens; the vault asks the desk's prompt // itself, in its own name, naming the asker as recorded and giving a key made for this one call; what is typed // comes back sealed to that key and opens here. How the prompt ended is the answer and the record's. func (v *Vault) AskAtDesk(caller string, a AskOf) (Asked, error) { if v.Ask == nil { return Asked{}, errors.New("nobody was asked to type anything: this vault reaches no desk") } opened, err := v.AskOpen(caller, a) if err != nil { return Asked{}, err } out := Asked{Secret: opened.Secret, AskedBy: opened.AskedBy} end := func(outcome string) (Asked, error) { out.Outcome = outcome if _, err := v.AskEnd(caller, opened.Call, "", outcome); err != nil { return out, err } return out, nil } raw, err := v.Ask(DeskPrompt(a.Desk), map[string]any{"module": a.Module, "secret": a.Name, "node": a.Node, "asked_by": opened.AskedBy, "seal_to": opened.SealTo, "timeout_seconds": DeskPromptFor}) if err != nil { if _, ended := end("refused"); ended != nil { v.log("%v", ended) } return Asked{}, fmt.Errorf("the desk on %s could not be asked: %w", a.Desk, err) } var answer struct { Sealed string `json:"sealed"` Cancelled bool `json:"cancelled"` TimedOut bool `json:"timed_out"` } if err := json.Unmarshal(raw, &answer); err != nil { return end("failed") } switch { case answer.TimedOut: return end("timed-out") case answer.Cancelled: return end("dismissed") case answer.Sealed == "": return end("failed") } changed, err := v.AskEnd(caller, opened.Call, answer.Sealed, "") if err != nil { // AskEnd recorded how it ended; an empty answer is the operator's, not a failure. if errors.Is(err, ErrNothingGiven) { out.Outcome = "empty" return out, nil } return Asked{}, err } out.Outcome, out.Generation, out.Unchanged = "given", changed.Generation, changed.Unchanged return out, nil } // ErrNothingGiven is a prompt answered empty: the operator's answer, not a failure. var ErrNothingGiven = errors.New("nothing was given") // Issued is what the vault kept of a value a provider's software issued. Never the value. For a tool that answers a // value per holder (perholder.go), Secret is the provision's name, Holders what was kept for each and NotAnswered // the holders asked about that the tool gave nothing for; Unchanged is then whether no holder's value changed. type Issued struct { Secret string `json:"secret"` Generation int `json:"generation"` Unchanged bool `json:"unchanged,omitempty"` Holders []IssuedHolder `json:"holders,omitempty"` NotAnswered []Holder `json:"not_answered,omitempty"` } // TakeIssued is `issued` (ADR 0349 clause 10, ADR 0305 with the vault in the controller's place): asked by the // controller alone, naming the provision and the provider's machine, module and tool. The vault asks that tool // itself, in its own name, with a key made for this one call; the tool answers its software's value sealed to that // key, which opens here and is kept as the provision's one credential. The bus lets the vault's account call no // tool but an issuing one, so a tool named here that is not one is refused there. func (v *Vault) TakeIssued(caller, provision, provider, module, tool string) (Issued, error) { return v.TakeIssuedFor(caller, provision, provider, module, tool, nil) } // TakeIssuedFor is TakeIssued naming the provision's holders from that provider, as the controller knows them (issue // 749): the tool is asked with them, and may answer one value for the provision (`sealed`, kept as its one // credential, as before) or one per holder (`holders`: node, module and sealed each, kept as that holder's own and // handed to it alone, perholder.go). Never both. func (v *Vault) TakeIssuedFor(caller, provision, provider, module, tool string, holders []Holder) (Issued, error) { if err := fromAGrantedCaller(caller, "issued"); err != nil { return Issued{}, err } return v.takeIssuedFor(caller, provision, provider, module, tool, holders) } // takeIssuedFor is TakeIssuedFor for whoever asks: the controller's verb, or the vault's own pass (ownIssue). caller // is who the kept values are recorded as taken by. func (v *Vault) takeIssuedFor(caller, provision, provider, module, tool string, holders []Holder) (Issued, error) { if v.Ask == nil { return Issued{}, errors.New("nothing was taken: this vault reaches no provider") } if !toolName.MatchString(tool) { return Issued{}, fmt.Errorf("the provider's tool %q is not a tool's name", tool) } if err := checkHolders(holders); err != nil { return Issued{}, fmt.Errorf("nothing was asked: %w", err) } opened, err := v.openIssued(provision, provider, module) if err != nil { return Issued{}, err } args := map[string]any{"provision": provision, "seal_to": opened.SealTo} if len(holders) > 0 { args["holders"] = holders } raw, err := v.Ask(module+"."+tool+"@"+provider, args) if err != nil { v.dropCall("issued", opened.Call) return Issued{}, fmt.Errorf("its tool %s could not be asked: %w", tool, err) } var given struct { Sealed string `json:"sealed"` Holders []sealedFor `json:"holders"` } if err := json.Unmarshal(raw, &given); err != nil || (given.Sealed == "") == (len(given.Holders) == 0) { v.dropCall("issued", opened.Call) return Issued{}, fmt.Errorf("its tool %s answered no sealed value, or both one value and a value per holder", tool) } if len(given.Holders) > 0 { return v.takePerHolder(caller, opened.Call, provider, provision, holders, given.Holders) } changed, err := v.IssuedTake(caller, opened.Call, given.Sealed) if err != nil { return Issued{}, err } return Issued{Secret: changed.Name, Generation: changed.Generation, Unchanged: changed.Unchanged}, nil } var toolName = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,62}$`) // dropCall forgets a call's key that will not be answered. func (v *Vault) dropCall(verb, id string) { v.callsMu.Lock() defer v.callsMu.Unlock() if c, ok := v.pending[id]; ok && c.verb == verb { c.private = [32]byte{} delete(v.pending, id) } } // ---- 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 person the controller names, beside the controller that // carried the act — both are kept — and why is said in the vault's log. // // **From the controller alone, and with a why** (the review of issue 675): only the controller knows whether a // module's secrets are filled from the vault yet (ADR 0349 clause 17); a rotation asked of the vault for a module // still filled the old way would change a value its machine never reads, and drift from the one the controller // holds. And a rotation is an act by hand, which says why. func (v *Vault) Rotate(caller, secret, by, why string) (Changed, error) { return v.RotateFor(caller, secret, Narrow{}, by, why) } // Narrow is the one consumer a rotation of a per-login provider's credential is for (issue 767): the controller's // `rotate --consumer --module `, either or both. Empty is every consumer. type Narrow struct { Consumer string `json:"consumer,omitempty"` ConsumerModule string `json:"consumer_module,omitempty"` } func (n Narrow) empty() bool { return n.Consumer == "" && n.ConsumerModule == "" } func (n Narrow) holds(r pairRow) bool { return (n.Consumer == "" || r.consumer == n.Consumer) && (n.ConsumerModule == "" || r.module == n.ConsumerModule) } func (n Narrow) String() string { switch { case n.Consumer == "": return n.ConsumerModule case n.ConsumerModule == "": return n.Consumer } return n.ConsumerModule + " on " + n.Consumer } // RotateFor is Rotate, narrowed to one consumer's own login of a per-login provider (ADR 0158 §11, issue 767). A // value shared by every holder (ADR 0158) is one value, so it is never rotated for one consumer alone: refused, and // nothing changes. A module's own secret names no consumer. func (v *Vault) RotateFor(caller, secret string, one Narrow, by, why string) (Changed, error) { if err := fromAGrantedCaller(caller, "rotate"); err != nil { return Changed{}, err } 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`)") } if err := validName(secret); err != nil { return Changed{}, err } rec, held, err := v.Store.Read(secret) if err != nil { return Changed{}, err } parts := strings.Split(secret, "/") shared := len(parts) == 3 && parts[0] == "provision" switch { case !one.empty() && !shared: return Changed{}, fmt.Errorf("not rotated: %s is not a provision's credential, and only a provider's "+ "consumers are named in a rotation", secret) case !one.empty() && held: return Changed{}, fmt.Errorf("not rotated: %s is one value every holder shares (ADR 0158), so it cannot be "+ "rotated for %s alone; rotated without a consumer, it is made anew for every holder", secret, one) case !held && shared: // A provider whose consumers keep logins of their own: each one's own is remade, never one shared value. changed, err := v.rotatePerPair(secret, one, someoneOr(by, caller)) if err == nil { v.log("rotated each consumer's own login of %s (%d), asked by %s via %s: %s", secret, len(changed.Each), someoneOr(by, caller), caller, strings.TrimSpace(why)) } return changed, err case !held: return Changed{}, fmt.Errorf("not rotated: the vault holds no secret named %s", secret) case Covered(secret): return Changed{}, fmt.Errorf("not rotated: %s is a bus account the controller mints and hands here (ADR 0374 "+ "clause 4): only a password whose hash the bus's user list holds can log in, so the vault makes no new "+ "value for it; `module issue` again hands a new one and replaces this in place", secret) case isLedgersSecret(secret): return Changed{}, fmt.Errorf("not rotated: %s is the tool runner's own secret of the ledger accounts of its machine, "+ "which the vault rewrites on each ledger account minted or retired (ADR 0597 §9); a person's rotation would "+ "replace every module's credential with a value no ledger account holds", 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 shared || (parts[0] == "pair" && (len(parts) == 5 || len(parts) == 6)) { if why := v.rotationHeldBack(strings.Join(append([]string{"provision"}, parts[1:3]...), "/")); why != "" { return Changed{}, fmt.Errorf("not rotated: %s", why) } } if by == "" { by = caller } raw := make([]byte, 30) if _, err := rand.Read(raw); err != nil { return Changed{}, err } changed, err := v.keep(secret, base64.RawURLEncoding.EncodeToString(raw), by, caller, OriginMade) if err == nil && shared { if sh, recorded, rerr := v.Store.ReadShared(secret); rerr == nil && recorded { sh.Generation, sh.At = changed.Generation, v.Now().UTC() if werr := v.Store.WriteShared(sh); werr != nil { v.log("%s was rotated, but the record of who holds it keeps the earlier generation: %v", secret, werr) } } } if err == nil { v.log("rotated %s (generation %d), asked by %s via %s: %s", secret, changed.Generation, by, caller, strings.TrimSpace(why)) } return changed, err } func someoneOr(by, caller string) string { if strings.TrimSpace(by) == "" { return caller } return by }