775 lines
31 KiB
Go
775 lines
31 KiB
Go
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/<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"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"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
|
|
// 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).
|
|
move *MoveOf
|
|
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 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
|
|
}
|
|
|
|
// 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{} }()
|
|
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`) and, for the machine's provisioner client, its own (`provisioner-broker`).
|
|
var CoveredSecrets = []string{"broker", "provisioner-broker"}
|
|
|
|
// Covered says whether secret is a covered account: a module's own secret named one of CoveredSecrets. 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 {
|
|
parts := strings.Split(secret, "/")
|
|
return len(parts) == 4 && parts[0] == "own" && slices.Contains(CoveredSecrets, parts[3])
|
|
}
|
|
|
|
// 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
|
|
}
|
|
return v.keep(c.secret, string(value), someoneOr(c.by, caller), caller, origin)
|
|
}
|
|
|
|
// ---- 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) {
|
|
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 := fromTheController(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 := 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)
|
|
}
|
|
|
|
// ---- 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 alone, which names who
|
|
// asked it. 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 := fromTheController(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 <provision> --consumer <machine> --module <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 caller != ControllerCaller {
|
|
return Changed{}, fmt.Errorf("the vault rotates for the controller alone, which knows whether the module's "+
|
|
"secrets are filled from the vault yet; %s is not the controller: `secret rotate` at the controller", caller)
|
|
}
|
|
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 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
|
|
}
|