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

202 lines
6.7 KiB
Go

package main
// A value a provider's software issued for each holder apart (design 57's V2c, issue 749; ADR 0349 clauses 10 and
// 11). The forge mints one read token per agent account (ADR 0365 clause 4): one value per provision, sealed to
// every holder alike, cannot carry that. So an issuing provider's tool may answer one value per holder — a holder
// being a consuming module on one machine — and the vault keeps each as that holder's own and hands it to that
// holder alone. A tool that answers one value keeps the provision's one credential, as before.
//
// Names in the store: `provision/<provider node>/<provision>/<holder node>/<holder module>`.
import (
"errors"
"fmt"
"strings"
)
// Holder is who one issued value is for: a consuming module on one machine.
type Holder struct {
Node string `json:"node"`
Module string `json:"module"`
}
func (h Holder) String() string { return h.Module + " on " + h.Node }
// HolderSecret is the vault's name for the value a provider's software issued for one holder of a provision.
func HolderSecret(provider, provision string, h Holder) (string, error) {
base, err := ProvisionSecret(provider, provision)
if err != nil {
return "", err
}
if err := h.valid(); err != nil {
return "", err
}
return base + "/" + h.Node + "/" + h.Module, nil
}
func (h Holder) valid() error {
if !namePart.MatchString(h.Node) {
return fmt.Errorf("the holder's machine %q is not a name the mesh writes", h.Node)
}
if !namePart.MatchString(h.Module) {
return fmt.Errorf("the holder's module %q is not a name the mesh writes", h.Module)
}
return nil
}
// holderOf is the holder a secret's name is for, when it is one holder's issued value.
func holderOf(name string) (Holder, bool) {
parts := strings.Split(name, "/")
if len(parts) != 5 || parts[0] != "provision" {
return Holder{}, false
}
return Holder{Node: parts[3], Module: parts[4]}, true
}
// isTheHolder is whether caller, the bus user the runtime proved, is the holder: its machine's provisioner client
// (`provisioner.<node>`, an account of its own, mesh-controller internal/broker) or the module itself under an
// account of its own (`<node>.<module>`, ADR 0259 §8's runs-as). Any other caller is not, whatever else allows it.
func isTheHolder(caller string, h Holder) bool {
return caller == "provisioner."+h.Node || caller == h.Node+"."+h.Module
}
// notTheHolder says why caller may not have a holder's value, or "".
func notTheHolder(caller, name string) string {
h, per := holderOf(name)
if !per || isTheHolder(caller, h) {
return ""
}
return fmt.Sprintf("%s was issued for %s alone, and is handed only to that holder: provisioner.%s or %s.%s",
name, h, h.Node, h.Node, h.Module)
}
// checkHolders refuses a list of holders that is not names the mesh writes, each once.
func checkHolders(holders []Holder) error {
seen := map[Holder]bool{}
for _, h := range holders {
if err := h.valid(); err != nil {
return err
}
if seen[h] {
return fmt.Errorf("%s is named twice", h)
}
seen[h] = true
}
return nil
}
// IssuedHolder is what the vault kept for one holder. Never the value.
type IssuedHolder struct {
Holder
Secret string `json:"secret"`
Generation int `json:"generation"`
Unchanged bool `json:"unchanged,omitempty"`
}
// sealedFor is one holder's value as the tool answers it: sealed to the call's key.
type sealedFor struct {
Node string `json:"node"`
Module string `json:"module"`
Sealed string `json:"sealed"`
}
// takePerHolder opens every holder's value the tool sealed to the call's key and keeps each as that holder's own.
// It takes the answer whole or not at all: a holder not asked about, a holder twice, a value not sealed to the call
// or an empty one keeps nothing for anybody. A holder asked about and not answered is said, and keeps nothing.
func (v *Vault) takePerHolder(caller, call, provider, provision string, asked []Holder, answered []sealedFor) (Issued, error) {
c, err := v.takeCall("issued", call)
if err != nil {
return Issued{}, err
}
defer func() { c.private = [32]byte{} }()
if len(asked) == 0 {
return Issued{}, fmt.Errorf("%s answered a value per holder, and no holder was named to it; nothing was taken", c.by)
}
want := map[Holder]bool{}
for _, h := range asked {
want[h] = true
}
type opened struct {
holder Holder
secret string
value string
}
var all []opened
got := map[Holder]bool{}
for _, a := range answered {
h := Holder{Node: a.Node, Module: a.Module}
switch {
case !want[h]:
return Issued{}, fmt.Errorf("%s answered a value for %s, which it was not asked about; nothing was taken", c.by, h)
case got[h]:
return Issued{}, fmt.Errorf("%s answered %s twice; nothing was taken", c.by, h)
}
got[h] = true
raw, err := c.openOne(a.Sealed)
if err != nil {
return Issued{}, fmt.Errorf("%s's value: %w", h, err)
}
value := strings.TrimSpace(string(raw))
zero(raw)
if value == "" {
return Issued{}, fmt.Errorf("%s answered an empty value for %s; nothing was taken", c.by, h)
}
secret, err := HolderSecret(provider, provision, h)
if err != nil {
return Issued{}, err
}
all = append(all, opened{holder: h, secret: secret, value: value})
}
base, _ := ProvisionSecret(provider, provision)
out := Issued{Secret: base, Unchanged: true}
var failed []error
for i := range all {
changed, err := v.keep(all[i].secret, all[i].value, c.by, caller, OriginIssued)
all[i].value = ""
if err != nil {
failed = append(failed, fmt.Errorf("%s: %w", all[i].holder, err))
continue
}
out.Holders = append(out.Holders, IssuedHolder{Holder: all[i].holder, Secret: changed.Name,
Generation: changed.Generation, Unchanged: changed.Unchanged})
if !changed.Unchanged {
out.Unchanged = false
}
}
for _, h := range asked {
if !got[h] {
out.NotAnswered = append(out.NotAnswered, h)
}
}
if len(failed) > 0 {
return out, fmt.Errorf("not every holder's value was kept: %w", errors.Join(failed...))
}
return out, nil
}
// holdersArg reads the holders the controller names, refusing anything else.
func holdersArg(a map[string]any) ([]Holder, error) {
raw, ok := a["holders"]
if !ok || raw == nil {
return nil, nil
}
list, ok := raw.([]any)
if !ok {
return nil, errors.New("holders is a list of {node, module}")
}
out := make([]Holder, 0, len(list))
for _, item := range list {
m, ok := item.(map[string]any)
if !ok {
return nil, errors.New("holders is a list of {node, module}")
}
node, nok := m["node"].(string)
module, mok := m["module"].(string)
if !nok || !mok || len(m) != 2 {
return nil, errors.New("each holder is {node, module} and nothing else")
}
out = append(out, Holder{Node: strings.TrimSpace(node), Module: strings.TrimSpace(module)})
}
return out, nil
}