Compose the credential half of the rendering over an Input: grants, own secrets, core boxes, the certificate, the bus's user list (issue 1364). Ported from mesh-control 6c2a8d62 plan.go renderingFor, grantsFor, certificateFor and core_box.go in their Reading and Foreseeing modes: nothing is made, kept or written; a stand-in is named foreseen, a core box to keep is returned, a consumer left out of a grant and a credential bound elsewhere are returned naming them. The bus's user list is an Input value the controller composes: the broker's account composer (internal/broker, about 900 lines that import the lease and the bus client) is not ported, see the report of this branch.

This commit is contained in:
2026-10-20 06:41:58 +00:00
parent aecb52c1af
commit b7e1c22512
2 changed files with 566 additions and 0 deletions
+440
View File
@@ -0,0 +1,440 @@
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) {
var issued []Issued
for _, row := range c.in.Issued {
if row.Provider == node {
issued = append(issued, row)
}
}
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
}
// 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
}
}
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
}
+126
View File
@@ -0,0 +1,126 @@
package compose
import (
"fmt"
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// Rendered is everything a node's declaration is composed with (plan.go renderingFor), over an Input: the place half (Places)
// and the credential half. Nothing was allocated, made, kept or written; what a send would do is returned for the caller to do.
type Rendered struct {
Rendering catalogue.Rendering
// ForeseenPorts are the ports the mesh may assign that the Input holds none for ("<module>:<port>").
ForeseenPorts []string
// Foreseen are the stand-ins the composition used for what the Input does not hold: an own secret the next send makes
// ("<module>/<name>"), a grant's sealed copy, the certificate and the bus's user list.
Foreseen []string
// CoreBoxesWanted are the core credentials whose box the send must ask the vault for and keep.
CoreBoxesWanted []CoreBoxWanted
// Withheld are the consumers the grants leave out because their identity overflows the provision's bound (novox/hq ADR 0225),
// and Unbound those whose credential from this node is on record and whose own resolution binds them elsewhere (issue
// 274). The controller says them; a check refuses a change that adds one (ADR 0446 clause 1).
Withheld []catalogue.Overflow
Unbound []catalogue.Unbound
// Said are the lines the controller says while composing (the bus users left out for want of a credential).
Said []string
}
// Render composes the rendering of a node: the grants it must create, where it puts its ports, its own secrets, the vault's boxes,
// its certificate, the names of the mesh, the bus's user list and its membership. The refusals are the controller's, in its words;
// where more than one fault stands, the first reported may differ from the controller's, which interleaves the halves.
func Render(in Input, node string, mode Mode) (Rendered, error) {
return newComposer(in).render(node, mode)
}
func (c *composer) render(nodeName string, mode Mode) (Rendered, error) {
resolved, err := c.planFor(nodeName)
if err != nil {
return Rendered{}, err
}
node, err := c.in.node(nodeName)
if err != nil {
return Rendered{}, err
}
plan := resolved.Plan
var out Rendered
foreseen := func(s string) { out.Foreseen = append(out.Foreseen, s) }
grants, withheld, unbound, grantsForeseen, err := c.grantsFor(nodeName)
if err != nil {
return Rendered{}, err
}
placed, err := c.places(nodeName, resolved)
if err != nil {
return Rendered{}, err
}
r := placed.Rendering
out.ForeseenPorts = placed.ForeseenPorts
r.Grants, r.Withheld, r.Unbound = grants, withheld, unbound
out.Withheld, out.Unbound = withheld, unbound
for _, g := range grantsForeseen {
foreseen(g)
}
// And each module's own secrets: a module handed to the provisioner has its secrets from the vault through the client.
handing := catalogue.Rendering{Hands: r.Hands, Provisioner: r.Provisioner}
needed, foreseenOwn, err := c.ownSecretsFor(node, plan, handing, mode)
if err != nil {
return Rendered{}, err
}
r.Needed = needed
if mode == Foreseeing || len(foreseenOwn) > 0 {
r.Foreseen = foreseenOwn
}
var names []string
for module, secrets := range foreseenOwn {
for name := range secrets {
names = append(names, module+"/"+name)
}
}
sort.Strings(names)
for _, n := range names {
foreseen(n)
}
// The core's own credentials as the vault sealed them, for an engine that opens the vault's box.
if node.Stated && node.Contract >= VaultBoxContract {
sealed, notPlaced, wanted := c.coreBoxesFor(node, plan.Modules, handing)
r.VaultSealed, r.NotPlaced = sealed, notPlaced
out.CoreBoxesWanted = wanted
}
// And a certificate for this machine's name inside the mesh, when anything on it asks.
for _, m := range plan.Modules {
if m.Certificate == nil {
continue
}
certificate, authority, stood, err := c.certificateFor(node)
if err != nil {
return Rendered{}, err
}
r.Certificate, r.Authority = certificate, authority
if stood {
foreseen("the certificate and the authority of " + nodeName)
}
break
}
// The bus's user list, for the machine that runs the bus: names and hashes the Input carries, never passwords.
if holdsTheBus(plan.Modules) {
r.BusUsers = c.in.BusUsers
if r.BusUsers == "" {
r.BusUsers = ForeseenBusUsers
foreseen("the bus's user list")
}
if len(c.in.BusMissing) > 0 {
out.Said = append(out.Said, fmt.Sprintf("the bus's user list leaves out %d user(s) the mesh has minted no credential "+
"for: %s. Each is a user that cannot connect until one is issued", len(c.in.BusMissing), strings.Join(c.in.BusMissing, ", ")))
}
}
r.BusMembership = c.in.BusMemberships[nodeName]
out.Rendering = r
return out, nil
}