Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/vault.go
T

368 lines
13 KiB
Go

package main
// The vault's rules (novox/hq ADR 0349 §7 and §11; ADR 0113 §1, §2, §22): it holds every secret sealed at
// rest, with a copy sealed to the operator key; it hands one out only on request and reply, sealed to the
// asker's key, and only to an asker it allows; it says a secret changed — who, when, never a value.
import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"sync"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/seal"
)
// Seat is the role this module holds.
const Seat = "mesh-vault"
// ChangedEvent is what the vault raises when a secret it holds changes: who, when, which, its generation.
const ChangedEvent = "secret-changed"
// Vault holds the secrets.
type Vault struct {
Store *Store
// Own is the vault's own key; the private half never leaves its state directory.
Own seal.KeyPair
// Askers decides who may ask `get` for what. Nil allows nobody.
Askers Askers
// Assignments is the controller's assignments mirror, where the vault sees a consumer bind a provider and
// unbind (pair.go). Empty sees nothing, and makes no provision's credential.
Assignments Mirror
Emit func(event string, body map[string]any) error
// Ask calls a tool on the mesh as the vault, through the runtime that launched it, under the vault's own bus
// account (ADR 0259 §8's runs-as, issue 723): the desks' secret prompt and each issuing provider's tool, which
// the bus lets the vault's account alone call in its own name. Nil asks nobody.
Ask func(key string, body any) (json.RawMessage, error)
Now func() time.Time
Log func(format string, args ...any)
mu sync.Mutex
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
callsMu sync.Mutex
pending map[string]*callKey
}
// Changed is what one change to a secret was. Never its value.
type Changed struct {
Name string `json:"name"`
Generation int `json:"generation"`
At time.Time `json:"at"`
By string `json:"by"`
Unchanged bool `json:"unchanged,omitempty"`
// Each is, for a rotation of a provider whose consumers keep logins of their own, each one's own value remade.
Each []Changed `json:"each,omitempty"`
}
// Answer is `get`'s reply: the secret sealed to the asker's key, and which generation it is.
type Answer struct {
Name string `json:"name"`
Generation int `json:"generation"`
ChangedAt time.Time `json:"changed-at"`
Sealed seal.Box `json:"sealed"`
}
func (v *Vault) log(format string, args ...any) {
if v.Log != nil {
v.Log(format, args...)
}
}
// Put keeps value as the secret name: sealed to the vault's key and to the operator key, a new generation
// when it differs from the one held, and `secret-changed` said. The same value again changes nothing.
// Refused: a private key (it stays where it was made), and any value while the vault has no operator key —
// a secret only the vault could open is what ADR 0113 §22 rules out.
func (v *Vault) Put(name, value, by string) (Changed, error) {
return v.keep(name, value, by, "", OriginMade)
}
// keep is Put with where the value came from (origin) and the caller the runtime proved when it is not who
// by names (via).
func (v *Vault) keep(name, value, by, via, origin string) (Changed, error) {
return v.keepWith(name, value, by, via, origin, keepOptions{})
}
// keepOptions are what the move keeps beside a value (move.go): that it never replaces another, when the value
// was first made, and where it was moved from.
type keepOptions struct {
neverReplace bool
madeAt time.Time
movedFrom *MovedFrom
}
// keepWith is keep with the move's options.
func (v *Vault) keepWith(name, value, by, via, origin string, opt keepOptions) (Changed, error) {
if err := validName(name); err != nil {
return Changed{}, err
}
if value == "" {
return Changed{}, fmt.Errorf("%s: a secret is not empty", name)
}
if privateKeyIn(value) {
return Changed{}, fmt.Errorf("%s: that is a private key, and a private key stays where it was made and never reaches the vault (ADR 0113 §2)", name)
}
if by == "" {
by = "the vault"
}
v.mu.Lock()
defer v.mu.Unlock()
operator, err := v.Store.OperatorKey()
if err != nil {
return Changed{}, err
}
if operator == "" {
return Changed{}, fmt.Errorf("%s is not kept: the vault has no operator key to seal its copy to (ADR 0113 §22) — the mesh writes it with the export once `operator key set` has been run", name)
}
before, held, err := v.Store.Read(name)
if err != nil {
return Changed{}, err
}
if held {
if old, err := seal.Open(before.AtRest, v.Own.PrivateKey); err == nil && old == value {
return Changed{Name: name, Generation: before.Generation, At: before.ChangedAt, By: before.ChangedBy, Unchanged: true}, nil
}
if opt.neverReplace {
return Changed{}, fmt.Errorf("the vault already holds another value for %s (generation %d, changed by %s "+
"at %s), and the move never replaces a value the vault holds: nothing was taken", name,
before.Generation, before.ChangedBy, before.ChangedAt.Format(time.RFC3339))
}
}
atRest, err := seal.Seal(value, v.Own.PublicKey)
if err != nil {
return Changed{}, err
}
forOperator, err := operatorSeal(operator, []byte(value))
if err != nil {
return Changed{}, err
}
now := v.Now().UTC()
if via == by {
via = ""
}
rec := Record{Name: name, Generation: 1, AtRest: atRest,
Operator: OperatorCopy{Box: OperatorBox, Key: operator, Sealed: forOperator},
MadeAt: now, ChangedAt: now, ChangedBy: by, ChangedVia: via, Origin: origin}
if held {
rec.Generation, rec.MadeAt = before.Generation+1, before.MadeAt
} else if !opt.madeAt.IsZero() {
rec.MadeAt = opt.madeAt.UTC()
}
if opt.movedFrom != nil {
moved := *opt.movedFrom
moved.At = now
rec.MovedFrom = &moved
}
if err := v.Store.Write(rec); err != nil {
return Changed{}, err
}
c := Changed{Name: name, Generation: rec.Generation, At: now, By: by}
v.announce(c)
return c, nil
}
// Mint makes a fresh value for name and keeps it: 30 random bytes, base64url without padding — 40
// characters, the controller's shape, which fits the tightest backend a minted password reaches.
func (v *Vault) Mint(name, by string) (Changed, error) {
raw := make([]byte, 30)
if _, err := rand.Read(raw); err != nil {
return Changed{}, err
}
return v.Put(name, base64.RawURLEncoding.EncodeToString(raw), by)
}
// announce raises `secret-changed`: who, when, which and its generation — never a value or a sealed copy.
func (v *Vault) announce(c Changed) {
if v.Emit == nil {
return
}
body := map[string]any{"name": c.Name, "by": c.By, "at": c.At.Format(time.RFC3339Nano), "generation": c.Generation}
if err := v.Emit(ChangedEvent, body); err != nil {
v.log("%s is kept at generation %d, but %s could not be said: %v", c.Name, c.Generation, ChangedEvent, err)
}
}
// recordHanding keeps who asked for what, and what they got, in the vault's own store, and says loudly
// when the record had to be rolled to make room.
func (v *Vault) recordHanding(h Handing) error {
h.At = v.Now().UTC()
rolled, err := v.Store.RecordHanding(h)
if err != nil {
v.log("the record of %s asking for %s could not be written: %v", h.To, h.Name, err)
return err
}
if rolled {
v.log("THE RECORD OF HAND-OUTS WAS ROLLED (%d times since the vault started): 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_handed and vault_status",
v.Store.Rolled(), v.Store.HandedBound(), HandedKept)
}
return nil
}
// busUser is the shape of a caller the runtime proves (the SDK's): the one shape a refusal is recorded for.
var busUser = regexp.MustCompile(`^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$`)
// Get answers caller with the secret name sealed to publicKey, if caller may ask for it. The caller is the
// bus user the runtime proved, never an argument; an unproven call is refused.
func (v *Vault) Get(caller, name, publicKey string) (Answer, error) {
if caller == "" || caller == stdio.Unproven {
return Answer{}, errors.New("the vault answers only a caller the runtime proved")
}
askers := v.Askers
if askers == nil {
askers = NoAsker{}
}
if !busUser.MatchString(caller) || len(caller) > 200 {
return Answer{}, errors.New("the vault answers only a caller the runtime proved")
}
// A name is judged before anything is recorded, so an arbitrary argument fills no record.
if err := validName(name); err != nil {
return Answer{}, err
}
// A value issued for one holder is that holder's alone, whatever else would allow it (issue 749).
if why := notTheHolder(caller, name); 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)
}
// The core's own credentials, to the controller alone, sealed only to their machine's key (ADR 0349 §16).
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.
v.log("not yet %s to %s: %s", name, caller, reason)
return Answer{}, fmt.Errorf("the vault holds no secret named %s yet: %s", name, reason)
}
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)
}
key, err := askerKey(publicKey)
if err != nil {
return Answer{}, err
}
rec, held, err := v.Store.Read(name)
if err != nil {
return Answer{}, err
}
if !held {
return Answer{}, fmt.Errorf("the vault holds no secret named %s", name)
}
value, err := seal.Open(rec.AtRest, v.Own.PrivateKey)
if err != nil {
return Answer{}, fmt.Errorf("%s does not open with the vault's key: %w", name, err)
}
sealed, err := seal.Seal(value, key)
if err != nil {
return Answer{}, err
}
if err := v.recordHanding(Handing{To: caller, Name: name, Generation: rec.Generation, Outcome: "handed"}); err != nil {
return Answer{}, fmt.Errorf("%s is not handed out: who was handed it could not be recorded: %w", name, err)
}
v.log("handed %s generation %d to %s, sealed to its key", name, rec.Generation, caller)
return Answer{Name: name, Generation: rec.Generation, ChangedAt: rec.ChangedAt, Sealed: sealed}, nil
}
// SetOperatorKey makes key (SPKI PEM) the operator key, then seals again to it every operator copy that is
// not yet — see Reseal. It returns how many copies it sealed again.
//
// The key is recorded first and each record says which key its copy is sealed to, so that record is the
// progress: a failure partway leaves the rest for the next call, and the same key given again (every minute
// from the mesh's export) finishes them. Nothing returns early while a copy is sealed to another key.
func (v *Vault) SetOperatorKey(key, by string) (int, error) {
raw, err := checkOperatorKey(key)
if err != nil {
return 0, err
}
v.mu.Lock()
defer v.mu.Unlock()
current, err := v.Store.OperatorKey()
if err != nil {
return 0, err
}
if current != raw {
if err := v.Store.setOperatorKey(raw, by, v.Now().UTC()); err != nil {
return 0, err
}
v.log("the operator key is now %s (by %s); every operator copy is sealed again to it", raw, by)
}
return v.reseal(raw)
}
// Reseal seals again to the current operator key every copy sealed to another one, and says how many it
// did. Called every minute, so a copy a failure left behind is not left for ever.
func (v *Vault) Reseal() (int, error) {
v.mu.Lock()
defer v.mu.Unlock()
key, err := v.Store.OperatorKey()
if err != nil || key == "" {
return 0, err
}
return v.reseal(key)
}
func (v *Vault) reseal(key string) (int, error) {
all, err := v.Store.List()
if err != nil {
return 0, err
}
n := 0
var failed []error
for _, rec := range all {
if rec.Operator.Key == key && rec.Operator.Box == OperatorBox {
continue
}
value, err := seal.Open(rec.AtRest, v.Own.PrivateKey)
if err != nil {
failed = append(failed, fmt.Errorf("%s does not open with the vault's key: %w", rec.Name, err))
continue
}
sealed, err := operatorSeal(key, []byte(value))
if err == nil {
rec.Operator = OperatorCopy{Box: OperatorBox, Key: key, Sealed: sealed}
err = v.Store.Write(rec)
}
if err != nil {
failed = append(failed, fmt.Errorf("%s: %w", rec.Name, err))
continue
}
n++
}
if len(failed) > 0 {
err := fmt.Errorf("%d operator copies are still sealed to an earlier key, tried again on the next pass: %w",
len(failed), errors.Join(failed...))
v.log("%v", err)
return n, err
}
return n, nil
}
// StillSealedToAnother is how many operator copies are not yet sealed to the current operator key.
func (v *Vault) StillSealedToAnother() (int, error) {
key, err := v.Store.OperatorKey()
if err != nil {
return 0, err
}
all, err := v.Store.List()
if err != nil {
return 0, err
}
n := 0
for _, rec := range all {
if rec.Operator.Key != key {
n++
}
}
return n, nil
}