mesh/delivery closed: a newer head of the same pull request
mesh/check checking 1faf31706919 on 5b75233406fe | novox/mesh-catalog#505 in group feat/1126-nodes-compose
289 lines
11 KiB
Go
289 lines
11 KiB
Go
package main
|
|
|
|
// A machine's node-engine composes its own modules (novox/hq ADR 0547 §2, issue 1126), and asks the vault for their own
|
|
// secrets sealed to its own key (ADR 0349 §11, ADR 0550 §5): the verb `own`, answered to the node-engine of a machine
|
|
// alone, about that machine alone (the machine is the caller's, as the bus proved it, never an argument). Each value is
|
|
// sealed to the sealing key the machine's engine stated (`node-key`), so what crosses the bus opens on that machine only,
|
|
// and the composition carries it sealed (no composition carries a secret in the clear). A module handed to the
|
|
// provisioner is left out: its client fills its secrets from `get` (ADR 0349 §17).
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/seal"
|
|
)
|
|
|
|
// NodeOwnSecret is one own secret of a module on the asking machine, sealed to the machine's key.
|
|
type NodeOwnSecret struct {
|
|
Module string `json:"module"`
|
|
Name string `json:"name"`
|
|
Origin string `json:"origin"`
|
|
Generation int `json:"generation"`
|
|
Sealed seal.Box `json:"sealed"`
|
|
}
|
|
|
|
// OwnAnswer is what `own` answers: the machine, the sealing key every value is sealed to (raw X25519, base64, as its
|
|
// engine stated it), each own secret of its modules, and each pair credential it asked for that the vault holds.
|
|
type OwnAnswer struct {
|
|
Node string `json:"node"`
|
|
Key string `json:"key"`
|
|
Own []NodeOwnSecret `json:"own"`
|
|
Credentials []NodeCredential `json:"credentials,omitempty"`
|
|
Missing []CredentialAsked `json:"missing,omitempty"`
|
|
}
|
|
|
|
// CredentialAsked names one pair credential by its ends, as a node composing its own asks for it (mesh-sdk
|
|
// compose.CredentialKey): Role is `consumer` when the asking machine consumes it, `provider` when it grants it.
|
|
type CredentialAsked struct {
|
|
Role string `json:"role"`
|
|
Provision string `json:"provision"`
|
|
Consumer string `json:"consumer"`
|
|
ConsumerModule string `json:"consumer_module"`
|
|
Provider string `json:"provider"`
|
|
Local string `json:"local,omitempty"`
|
|
}
|
|
|
|
// NodeCredential is one pair credential answered, sealed to the asking machine's key, with the name the vault keeps it
|
|
// under (`pair/...` per pair, `provision/<provider>/<provision>/<node>/<module>` issued for one holder, or the provision's
|
|
// one shared value `provision/<provider>/<provision>`).
|
|
type NodeCredential struct {
|
|
CredentialAsked
|
|
Name string `json:"name"`
|
|
Generation int `json:"generation"`
|
|
Sealed seal.Box `json:"sealed"`
|
|
}
|
|
|
|
// credentialNames are the names a pair credential may be kept under, the most particular first.
|
|
func credentialNames(c CredentialAsked) []string {
|
|
pair := "pair/" + c.Provider + "/" + c.Provision + "/" + c.Consumer + "/" + c.ConsumerModule
|
|
if c.Local != "" {
|
|
pair += "/" + c.Local
|
|
}
|
|
return []string{pair, "provision/" + c.Provider + "/" + c.Provision + "/" + c.Consumer + "/" + c.ConsumerModule,
|
|
"provision/" + c.Provider + "/" + c.Provision}
|
|
}
|
|
|
|
// credentialRefusal says why node may not have c, or "": a consumer's credential to the machine its consuming module
|
|
// stands on, a provider's to the providing machine, and nothing to a machine that is neither.
|
|
func (v *Vault) credentialRefusal(node string, c CredentialAsked) string {
|
|
for _, part := range []string{c.Provision, c.Consumer, c.ConsumerModule, c.Provider} {
|
|
if !namePart.MatchString(part) {
|
|
return fmt.Sprintf("%q is not a name the mesh writes", part)
|
|
}
|
|
}
|
|
if c.Local != "" && !namePart.MatchString(c.Local) {
|
|
return fmt.Sprintf("%q is not a name the mesh writes", c.Local)
|
|
}
|
|
switch c.Role {
|
|
case "consumer":
|
|
if c.Consumer != node {
|
|
return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer)
|
|
}
|
|
// The consuming module stands on the machine: read, never assumed (an assignment that cannot be read refuses).
|
|
read, why := v.assignmentReader(node)
|
|
if why != "" {
|
|
return why
|
|
}
|
|
raw, err := read(node + "." + c.ConsumerModule)
|
|
if err != nil {
|
|
return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err)
|
|
}
|
|
if raw == nil {
|
|
return fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", c.ConsumerModule, node)
|
|
}
|
|
return ""
|
|
case "provider":
|
|
if c.Provider != node {
|
|
return fmt.Sprintf("%s asks as the provider %s, and is not that machine", node, c.Provider)
|
|
}
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("the role %q is not consumer or provider", c.Role)
|
|
}
|
|
|
|
// assignmentReader is the vault's reader of the assignments, or why it has none: the rule `own` keeps reads them,
|
|
// and without them it refuses rather than hands (a rule that cannot be read is no rule).
|
|
func (v *Vault) assignmentReader(node string) (AssignmentReader, string) {
|
|
d, ok := v.Askers.(Declared)
|
|
if !ok || d.Read == nil {
|
|
return nil, "the vault cannot read the assignments, so it cannot tell what " + node + "'s modules are; nothing is handed"
|
|
}
|
|
return standing(d.Read), ""
|
|
}
|
|
|
|
// sharedRefusal says why node may not have the provision's one shared value `provision/<provider>/<provision>` for c,
|
|
// or "": the vault's record of who holds it names the consumer's module among its consumers, or the machine as its
|
|
// provider. Without that record nothing is handed: the shared value is not one any machine may have by naming a
|
|
// provider (ADR 0550 §5).
|
|
func (v *Vault) sharedRefusal(node string, c CredentialAsked) (string, error) {
|
|
name := "provision/" + c.Provider + "/" + c.Provision
|
|
sh, ok, err := v.Store.ReadShared(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !ok {
|
|
return fmt.Sprintf("the vault keeps no record of who holds %s", name), nil
|
|
}
|
|
switch c.Role {
|
|
case "consumer":
|
|
want := consumerOf(c.Consumer, c.ConsumerModule, c.Local)
|
|
for _, held := range sh.Consumers {
|
|
if held == want {
|
|
return "", nil
|
|
}
|
|
}
|
|
return fmt.Sprintf("%s is held by %s, not by %s", name, strings.Join(sh.Consumers, ", "), want), nil
|
|
case "provider":
|
|
if sh.Provider == node {
|
|
return "", nil
|
|
}
|
|
return fmt.Sprintf("%s is provided from %s, not %s", name, sh.Provider, node), nil
|
|
}
|
|
return fmt.Sprintf("the role %q is not consumer or provider", c.Role), nil
|
|
}
|
|
|
|
// composeOrigin is the vault's origin of a value as the composition reads it: made by the mesh, or given (accepted at a
|
|
// terminal, typed at a desk, issued by a provider's software).
|
|
func composeOrigin(origin string) string {
|
|
if origin == "" || origin == OriginMade {
|
|
return OriginMade
|
|
}
|
|
return OriginAccepted
|
|
}
|
|
|
|
// OwnOfNode answers the node-engine of a machine the own secrets of its modules, each sealed to the machine's stated key.
|
|
func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, error) {
|
|
node, ok := nodeEngineOf(caller)
|
|
if !ok {
|
|
if caller == "" || caller == stdio.Unproven {
|
|
caller = "an unproven caller"
|
|
}
|
|
return OwnAnswer{}, fmt.Errorf("own answers a machine's node-engine alone, about its own machine; %s is not a node-engine", caller)
|
|
}
|
|
key, had, err := v.Store.NodeKey(node)
|
|
if err != nil {
|
|
return OwnAnswer{}, err
|
|
}
|
|
if !had {
|
|
return OwnAnswer{}, fmt.Errorf("the vault holds no secret named own/%s yet: %s's node-engine has stated no sealing key "+
|
|
"(node-key), so there is no key of its own to seal to", node, node)
|
|
}
|
|
to, err := publicPEM(key)
|
|
if err != nil {
|
|
return OwnAnswer{}, fmt.Errorf("%s's sealing key as recorded is not usable: %w", node, err)
|
|
}
|
|
all, err := v.Store.List()
|
|
if err != nil {
|
|
return OwnAnswer{}, err
|
|
}
|
|
out := OwnAnswer{Node: node, Key: key}
|
|
for _, r := range all {
|
|
parts := strings.Split(r.Name, "/")
|
|
if len(parts) != 4 || parts[0] != "own" || parts[1] != node {
|
|
continue
|
|
}
|
|
handed, err := v.handed(node, parts[2])
|
|
if err != nil {
|
|
return OwnAnswer{}, err
|
|
}
|
|
if handed {
|
|
continue
|
|
}
|
|
value, err := seal.Open(r.AtRest, v.Own.PrivateKey)
|
|
if err != nil {
|
|
return OwnAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", r.Name, err)
|
|
}
|
|
sealed, err := seal.Seal(value, to)
|
|
if err != nil {
|
|
return OwnAnswer{}, err
|
|
}
|
|
if err := v.recordHanding(Handing{To: caller, Name: r.Name, Generation: r.Generation, Outcome: "handed"}); err != nil {
|
|
return OwnAnswer{}, fmt.Errorf("%s is not handed out: who was handed it could not be recorded: %w", r.Name, err)
|
|
}
|
|
out.Own = append(out.Own, NodeOwnSecret{Module: parts[2], Name: parts[3], Origin: composeOrigin(r.Origin),
|
|
Generation: r.Generation, Sealed: sealed})
|
|
}
|
|
sort.Slice(out.Own, func(i, j int) bool {
|
|
if out.Own[i].Module != out.Own[j].Module {
|
|
return out.Own[i].Module < out.Own[j].Module
|
|
}
|
|
return out.Own[i].Name < out.Own[j].Name
|
|
})
|
|
for _, c := range asked {
|
|
if why := v.credentialRefusal(node, c); why != "" {
|
|
return OwnAnswer{}, fmt.Errorf("%s may not ask for the credential of %s from %s to %s on %s: %s", caller,
|
|
c.Provision, c.Provider, c.ConsumerModule, c.Consumer, why)
|
|
}
|
|
found := false
|
|
names := credentialNames(c)
|
|
for i, name := range names {
|
|
r, held, err := v.Store.Read(name)
|
|
if err != nil {
|
|
return OwnAnswer{}, err
|
|
}
|
|
if !held {
|
|
continue
|
|
}
|
|
if i == len(names)-1 {
|
|
// The provision's one shared value: handed only to a machine the record of its holders names.
|
|
why, err := v.sharedRefusal(node, c)
|
|
if err != nil {
|
|
return OwnAnswer{}, err
|
|
}
|
|
if why != "" {
|
|
v.log("%s is not handed %s: %s", node, name, why)
|
|
continue
|
|
}
|
|
}
|
|
value, err := seal.Open(r.AtRest, v.Own.PrivateKey)
|
|
if err != nil {
|
|
return OwnAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", name, err)
|
|
}
|
|
sealed, err := seal.Seal(value, to)
|
|
if err != nil {
|
|
return OwnAnswer{}, err
|
|
}
|
|
if err := v.recordHanding(Handing{To: caller, Name: name, Generation: r.Generation, Outcome: "handed"}); err != nil {
|
|
return OwnAnswer{}, fmt.Errorf("%s is not handed out: who was handed it could not be recorded: %w", name, err)
|
|
}
|
|
out.Credentials = append(out.Credentials, NodeCredential{CredentialAsked: c, Name: name, Generation: r.Generation, Sealed: sealed})
|
|
found = true
|
|
break
|
|
}
|
|
if !found {
|
|
out.Missing = append(out.Missing, c)
|
|
}
|
|
}
|
|
v.log("handed %s's node-engine the %d own secrets of its modules and %d credentials (%d not held), sealed to its key",
|
|
node, len(out.Own), len(out.Credentials), len(out.Missing))
|
|
return out, nil
|
|
}
|
|
|
|
// handed says the plan of module on node hands it to the provisioner, so its secrets are filled from `get`, not composed.
|
|
// A plan that cannot be read is an error, never "not handed": `own` refuses rather than hands a secret the provisioner
|
|
// holds.
|
|
func (v *Vault) handed(node, module string) (bool, error) {
|
|
read, why := v.assignmentReader(node)
|
|
if why != "" {
|
|
return false, errors.New(why)
|
|
}
|
|
raw, err := read(node + "." + module)
|
|
if err != nil {
|
|
return false, fmt.Errorf(notYetPrefix+"the assignment %s.%s could not be read, so whether it is handed to the "+
|
|
"provisioner is not known: %w", node, module, err)
|
|
}
|
|
if raw == nil {
|
|
return false, nil
|
|
}
|
|
var a plannedSecrets
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return false, fmt.Errorf("the assignment %s.%s is not the controller's: %w", node, module, err)
|
|
}
|
|
return a.Provisioning != nil, nil
|
|
}
|