202 lines
6.7 KiB
Go
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
|
|
}
|