368 lines
13 KiB
Go
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
|
|
}
|