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 another node that resolves, 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() { if other.Name == node { continue } 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 }