Files
mesh-sdk/go/compose/credentials.go
T
mesh-tools 075bf1e1fe
mesh/repo-check head 075bf1e1fe8f merged into trunk 88581d0acaef — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (typescript)…
mesh/merge-gate head 075bf1e1fe8f merged into trunk 88581d0acaef — pass: group feat/r4a-sdk-first composed: every machine composes with the change as it di…
mesh/delivery delivered
mesh/delivery-group group feat/r4a-sdk-first delivered: every member is delivered
Restore the line break the previous patch lost in rowsFor (formatting only)
2026-10-20 07:06:40 +00:00

496 lines
20 KiB
Go

package compose
import (
"crypto/ecdh"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Mode says what a composition does about what a send would make. Both modes read what the Input holds and make nothing; they
// differ as the controller's Reading and Foreseeing modes do.
type Mode int
const (
// Reading composes what is held and leaves out an own secret the Input holds none for: that machine cannot be running it.
Reading Mode = iota
// Foreseeing composes an own secret the next send would make with a stand-in and names it foreseen, while one the send would
// refuse (a bus credential nobody issued, a given secret under an old key) is refused here, in the same words.
Foreseeing
)
// ForeseenCertificate and ForeseenBusUsers stand for what the Input does not carry, in a composition asked ahead of the send.
// Never sent.
const (
ForeseenCertificate = "foreseen: issued by the next send"
ForeseenBusUsers = "foreseen: composed from the bus records by the next send"
)
// grantsFor is every credential this node must create, because something elsewhere uses it (plan.go grantsFor): the mirror of
// what a consumer is given. A consumer whose identity overflows the provision's bound is left out of the grants and returned
// beside them (novox/hq ADR 0225); one whose resolution binds it to another provider is withdrawn and returned beside them
// (issue 274). Every other is granted.
func (c *composer) grantsFor(node string) ([]catalogue.Grant, []catalogue.Overflow, []catalogue.Unbound, []string, error) {
issued, err := c.rowsFor(node)
if err != nil {
return nil, nil, nil, nil, err
}
onNetwork, err := c.whereEveryoneIs()
if err != nil {
return nil, nil, nil, nil, err
}
out := make([]catalogue.Grant, 0, len(issued))
var withheld []catalogue.Overflow
var unbound []catalogue.Unbound
var foreseen []string
for _, s := range issued {
resolved, err := c.planFor(s.Consumer)
switch {
case unresolvable(err):
// Their set does not resolve. Skipped rather than fatal: this node is not the place to report another machine's problem.
continue
case err != nil:
// The mesh could not be asked what they wanted, which is not the same as their wanting nothing.
return nil, nil, nil, nil, fmt.Errorf("what %s asked of %s cannot be read: %w", s.Consumer, s.Provision, err)
}
plan := resolved.Plan
values, asks, err := plan.ContributionsFrom(s.Provision, s.ConsumerModule, resolved.Settings)
if err != nil {
return nil, nil, nil, nil, err
}
// A port in there is the consumer's software port until this: the assignment that moved it is that machine's.
consumer, err := c.in.node(s.Consumer)
if err != nil {
return nil, nil, nil, nil, err
}
values = catalogue.AtPublishedPort(values, s.ConsumerModule, consumer.Ports[s.ConsumerModule])
from := s.ConsumerModule
if !asks {
// That module no longer wants this: left empty, which is how a login is withdrawn rather than kept working for ever.
from = ""
}
if bound := plan.BindsFrom(s.Provision, s.ConsumerModule, s.Local); from != "" && !slices.Contains(bound, node) {
// It still asks, and not of this node: withdrawn exactly as a credential nobody asks for, and said.
from = ""
unbound = append(unbound, catalogue.Unbound{Provision: s.Provision, Provider: node,
Consumer: s.Consumer, Module: s.ConsumerModule, Local: s.Local, BoundTo: bound})
}
// The consumer's identity slug, from its own manifest, carried on the grant so the provider derives the same login the
// consumer does (novox/hq ADR 0049).
slug := ""
for _, mm := range plan.Modules {
if mm.Module == s.ConsumerModule {
slug = mm.Slug
break
}
}
if from != "" {
bound := boundOfGrant(plan, s, node)
source := catalogue.IdentitySource(slug, s.ConsumerModule)
if catalogue.CheckIdentityWithin(s.Consumer, source, bound) != nil {
withheld = append(withheld, catalogue.Overflow{Provision: s.Provision, Provider: node,
Consumer: s.Consumer, Module: s.ConsumerModule,
Identity: catalogue.ConsumerIdentity(s.Consumer, source), Bound: bound})
continue
}
}
sealed := s.ForProvider
if sealed == "" {
sealed = catalogue.ForeseenSealed
foreseen = append(foreseen, fmt.Sprintf("the grant of %s for %s on %s", s.Provision, s.ConsumerModule, s.Consumer))
}
out = append(out, catalogue.Grant{
Provision: s.Provision, Consumer: s.Consumer, At: onNetwork[s.Consumer],
From: from, Values: values, Slug: slug, Sealed: sealed, Local: s.Local})
}
return out, withheld, unbound, foreseen, nil
}
// rowsFor is every pair credential this node's declaration must carry a grant for, in the order the controller reads them
// (SecretsFrom: by provision, consumer, consumer module and local name): the ones the Input says were made, and the ones the same send
// would make before it composes this node -- every need of a node that resolves (this one too), whose provider is this node, that is not
// answered by a record and not a credential only a person gives and nobody gave. Those have no sealed copy (ForProvider empty), so the
// grant is composed with a stand-in and named foreseen (plan.go: planFor makes the secret in every mode before grantsFor reads it).
func (c *composer) rowsFor(node string) ([]Issued, error) {
var rows []Issued
for _, row := range c.in.Issued {
if row.Provider == node {
rows = append(rows, row)
}
}
for _, other := range c.in.sorted() {
resolved, err := c.planFor(other.Name)
switch {
case unresolvable(err):
continue
case err != nil:
return nil, fmt.Errorf("what %s asked of %s cannot be read: %w", other.Name, node, err)
}
shelf := c.shelfFor(other)
for _, n := range resolved.Plan.Needs {
if n.From != node || n.ByRecord {
continue
}
cred, held := c.credential(n, other.Name)
if _, outside := issuedOutsideBy(shelf, n); cred.State == CredentialNotGiven || (outside && !(held && cred.State == CredentialHeld)) {
continue
}
if slices.ContainsFunc(rows, func(r Issued) bool {
return r.Provision == n.Name && r.Consumer == other.Name && r.ConsumerModule == n.For && r.Local == n.Local
}) {
continue
}
rows = append(rows, Issued{Provision: n.Name, Consumer: other.Name, ConsumerModule: n.For, Local: n.Local, Provider: node})
}
}
sort.SliceStable(rows, func(i, j int) bool {
a, b := rows[i], rows[j]
switch {
case a.Provision != b.Provision:
return a.Provision < b.Provision
case a.Consumer != b.Consumer:
return a.Consumer < b.Consumer
case a.ConsumerModule != b.ConsumerModule:
return a.ConsumerModule < b.ConsumerModule
}
return a.Local < b.Local
})
return rows, nil
}
// boundOfGrant is the identity bound the consumer's own resolution states for the requirement this grant answers. A requirement
// not found there is held to the tightest bound the mesh knows rather than to none.
func boundOfGrant(consumer catalogue.Resolution, s Issued, provider string) catalogue.IdentityBound {
for _, n := range consumer.Needs {
if n.Name == s.Provision && n.For == s.ConsumerModule && n.From == provider {
return n.Identity
}
}
return catalogue.DefaultIdentityBound
}
// ownSecretRow is the row for one own secret of a module on a node.
func (c *composer) ownSecretRow(node, module, name string) (OwnSecretRow, bool) {
for _, r := range c.in.OwnSecrets {
if r.Node == node && r.Module == module && r.Name == name {
return r, true
}
}
return OwnSecretRow{}, false
}
// givenMember is one own secret a person gave on a machine and whether it is sealed to the key the machine holds now.
type givenMember struct {
Name string
Sealed string
Current bool
}
// givenMembers is every member of a module's secret family given on a machine, by name (inv.GivenMembers): the mesh never
// makes a member, so only a value a person gave is one; a machine with no sealing key holds none.
func (c *composer) givenMembers(node Node, module, family string) []givenMember {
if node.SealingKey == "" {
return nil
}
prefix := catalogue.FamilyPrefix(family)
var out []givenMember
for _, r := range c.in.OwnSecrets {
if r.Node == node.Name && r.Module == module && r.Origin == "accepted" && strings.HasPrefix(r.Name, prefix) {
out = append(out, givenMember{Name: r.Name, Sealed: r.Sealed, Current: r.SealedTo == node.SealingKey})
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
// givenOwnSecret is an own secret issued outside the mesh as a person gave it on a machine (inv.GivenOwnSecret): not given when
// the value held was made by the mesh or the machine has no sealing key.
func (c *composer) givenOwnSecret(node Node, module, name string) (givenMember, bool) {
if node.SealingKey == "" {
return givenMember{}, false
}
r, ok := c.ownSecretRow(node.Name, module, name)
if !ok || r.Origin != "accepted" {
return givenMember{}, false
}
return givenMember{Name: name, Sealed: r.Sealed, Current: r.SealedTo == node.SealingKey}, true
}
// moduleSecretIfIssued is the read half of the controller's SecretForModule (inv.ModuleSecretIfIssued): the secret when one is
// held and sealed to the key the machine holds now, and otherwise not held. A key it was sealed to that the machine no longer
// has is reported as absent rather than as an error.
func (c *composer) moduleSecretIfIssued(node Node, module, name string) (sealed string, held bool) {
if node.SealingKey == "" {
return "", false
}
r, ok := c.ownSecretRow(node.Name, module, name)
if !ok || r.SealedTo != node.SealingKey {
return "", false
}
return r.Sealed, true
}
// wouldMakeSecretForModule says what the send would do about an own secret without doing it: nil when it would make one (or one
// is held), and otherwise the very refusal it would meet (inv.WouldMakeSecretForModule).
func (c *composer) wouldMakeSecretForModule(node Node, module, name string) error {
if node.SealingKey == "" {
return fmt.Errorf("%s needs a secret and %s has no sealing key, so nothing can be sealed to it", module, node.Name)
}
r, ok := c.ownSecretRow(node.Name, module, name)
switch {
case !ok:
return nil
case r.SealedTo != node.SealingKey && r.Origin == "accepted":
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
"since generated a new sealing key. The mesh cannot make another; issue it again", module, node.Name, name, node.Name)
}
return nil
}
// busCredentialIssued refuses an own secret called `broker` whose bus account nobody issued (novox/hq issue 203): the broker
// credential is never invented, an account on the bus minted by `module issue`.
func (c *composer) busCredentialIssued(node, module, name string) error {
var user string
switch name {
case "broker":
user = node + "." + module
case ProvisionerSecret:
user = "provisioner." + node
default:
return nil
}
if !slices.Contains(c.in.BusMinted, user) {
return fmt.Errorf(
"%s on %s has no bus credential: nothing was issued for %s, and a push "+
"would seal a placeholder its process cannot read (novox/hq issue 203). "+
"`module issue %s --node %s`, then push again",
module, node, user, module, node)
}
return nil
}
// givenUnderAnOldKey is the refusal for a value a person gave sealed to a key the machine no longer holds.
func givenUnderAnOldKey(module, node, name string) error {
return fmt.Errorf("%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
"since generated a new sealing key. The mesh cannot make another; give it again", module, node, name, node)
}
// ownSecretsFor is each module's own secrets as this node composes them (plan.go renderingFor): the needed ones sealed to the
// node, and, in the Foreseeing mode, the ones the next send would make named foreseen. A module handed to the provisioner has its
// secrets from the vault through the client, so none of them is composed here.
func (c *composer) ownSecretsFor(node Node, plan catalogue.Resolution, handing catalogue.Rendering, mode Mode) (
needed map[string]map[string]string, foreseen map[string]map[string]bool, err error) {
needed = map[string]map[string]string{}
foreseen = map[string]map[string]bool{}
mark := func(module, name string) {
if foreseen[module] == nil {
foreseen[module] = map[string]bool{}
}
foreseen[module][name] = true
}
for _, m := range plan.Modules {
if handing.Handing(m) {
continue
}
// A secret family is never made (novox/hq ADR 0283): each member a person gave on this machine is placed, and one not
// given is nothing -- the module says it waits for it.
for _, family := range m.OwnSecrets.Families() {
for _, g := range c.givenMembers(node, m.Module, family) {
if _, fam, ok := m.OwnSecrets.Lookup(g.Name); !ok || fam != family {
continue // a longer family's member, or a name no longer of this family
}
if !g.Current {
continue
}
if needed[m.Module] == nil {
needed[m.Module] = map[string]string{}
}
needed[m.Module][g.Name] = g.Sealed
if g.Sealed == "" {
// Given, and its value is not in the Input: placed with a stand-in, named.
needed[m.Module][g.Name] = catalogue.ForeseenSealed
mark(m.Module, g.Name)
}
}
}
names := make([]string, 0)
for name := range m.OwnSecrets.Plain() {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
own := m.OwnSecrets.Plain()[name]
// An own secret issued outside the mesh is never made (novox/hq issue 401, ADR 0228; ADR 0283 for a family): only a
// value a person gave is placed. One not given is nothing, and a value the mesh made for it before is not one either.
if own.IssuedBy == catalogue.IssuedOutside {
g, given := c.givenOwnSecret(node, m.Module, name)
if !given || !g.Current {
continue
}
if g.Sealed == "" {
mark(m.Module, name)
continue
}
if needed[m.Module] == nil {
needed[m.Module] = map[string]string{}
}
needed[m.Module][name] = g.Sealed
continue
}
sealed, held := c.moduleSecretIfIssued(node, m.Module, name)
if !held && mode == Foreseeing {
// Asked ahead of the send: what the send would do about it, by the send's own rules. Made by it -- a stand-in,
// named; refused by it -- refused here, the same words (novox/hq issue 275).
if err := c.busCredentialIssued(node.Name, m.Module, name); err != nil {
return nil, nil, err
}
if err := c.wouldMakeSecretForModule(node, m.Module, name); err != nil {
return nil, nil, err
}
mark(m.Module, name)
continue
}
if !held {
// Never issued, so this machine cannot be running it: left out rather than invented.
continue
}
if sealed == "" {
// Held, and its value is not in the Input: composed with a stand-in, named.
mark(m.Module, name)
continue
}
if needed[m.Module] == nil {
needed[m.Module] = map[string]string{}
}
needed[m.Module][name] = sealed
}
}
return needed, foreseen, nil
}
// CoreBoxWanted is a core credential whose box the Input does not hold for a machine: the next send asks the vault for it and
// keeps the answer. It is returned for the caller to ask and keep; this package asks and keeps nothing.
type CoreBoxWanted struct {
Node string
Module string
Name string
}
// coreModulesOf is every module of the plan on the core's list whose own secrets this machine's declaration carries, with their
// plain own secret names, sorted: one handed to the provisioner has its secrets from the vault through the client, and one issued
// outside the mesh is never the vault's to seal here.
func coreModulesOf(modules []catalogue.Manifest, handed func(catalogue.Manifest) bool) map[string][]string {
out := map[string][]string{}
for _, m := range modules {
if !catalogue.CarriesVaultSealed(m.Module) || (handed != nil && handed(m)) {
continue
}
var names []string
for name, own := range m.OwnSecrets.Plain() {
if own.IssuedBy != catalogue.IssuedOutside {
names = append(names, name)
}
}
if len(names) > 0 {
sort.Strings(names)
out[m.Module] = names
}
}
return out
}
// sealingKeyValid says a machine's sealing key, recorded raw in base64, is a raw X25519 key.
func sealingKeyValid(raw string) error {
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(raw))
if err != nil || len(b) != 32 {
return errors.New("the machine's sealing key is not a raw X25519 key")
}
_, err = ecdh.X25519().NewPublicKey(b)
return err
}
// coreBoxesFor decides each core credential of a machine in a composition that sends nothing and asks no vault (core_box.go
// coreBoxesOf, in the modes that are not a send): the kept box while it is sealed to the key the machine holds now, and otherwise a
// stand-in that the next send replaces by asking the vault -- returned in wanted for the caller to ask and keep. A machine with no
// sealing key, or one whose key is not a key, leaves its module out, said.
func (c *composer) coreBoxesFor(node Node, modules []catalogue.Manifest, handing catalogue.Rendering) (
sealed map[string]map[string]json.RawMessage, notPlaced map[string]string, wanted []CoreBoxWanted) {
core := coreModulesOf(modules, func(m catalogue.Manifest) bool { return handing.Handing(m) })
sealed, notPlaced = map[string]map[string]json.RawMessage{}, map[string]string{}
mods := make([]string, 0, len(core))
for m := range core {
mods = append(mods, m)
}
sort.Strings(mods)
for _, module := range mods {
if node.SealingKey == "" {
notPlaced[module] = fmt.Sprintf("%s has no sealing key, so the vault can seal %s's credentials to "+
"nothing there (novox/hq ADR 0349 §16)", node.Name, module)
continue
}
if err := sealingKeyValid(node.SealingKey); err != nil {
notPlaced[module] = fmt.Sprintf("%s's credentials cannot be asked of the vault: %v", module, err)
continue
}
boxes := map[string]json.RawMessage{}
for _, name := range core[module] {
var kept *CoreBox
for i := range c.in.KeptCoreBoxes {
k := &c.in.KeptCoreBoxes[i]
if k.Node == node.Name && k.Module == module && k.Name == name {
kept = k
}
}
if kept != nil && kept.SealedTo == node.SealingKey && len(kept.Box) > 0 {
boxes[name] = kept.Box
continue
}
boxes[name] = catalogue.ForeseenVaultBox
wanted = append(wanted, CoreBoxWanted{Node: node.Name, Module: module, Name: name})
}
sealed[module] = boxes
}
return sealed, notPlaced, wanted
}
// certificateFor is what the mesh certifies about one machine's internal name (plan.go certificateFor): refused for a machine that
// never told the mesh what key it serves with, and for one not on the private network, which has no name to be certified for.
// The certificate and the authority are public values the Input may carry; without them stand-ins are composed, and named.
func (c *composer) certificateFor(node Node) (certificate, authority string, foreseen bool, err error) {
if node.ServingKey == "" {
return "", "", false, fmt.Errorf(
"%s wants a certificate and has never told the mesh what key it serves with; it "+
"joins again to report one", node.Name)
}
where, err := c.whereEveryoneIs()
if err != nil {
return "", "", false, err
}
if where[node.Name] == "" {
return "", "", false, fmt.Errorf(
"%s wants a certificate and is not on the private network, so it has no name inside "+
"the mesh to be certified for", node.Name)
}
certificate, authority = node.Certificate, c.in.Authority
if certificate == "" || authority == "" {
return ForeseenCertificate, ForeseenCertificate, true, nil
}
return certificate, authority, false, nil
}
// holdsTheBus says whether a node's set includes the module that asks for the bus's user list.
func holdsTheBus(modules []catalogue.Manifest) bool {
for _, m := range modules {
if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") {
return true
}
}
return false
}