package main import ( "context" "errors" "fmt" "sort" "strings" "time" "github.com/nats-io/nats.go" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/secrets" ) // `secret issued `: the credential a provider's software issued, taken from the provider and kept as // the provision's one credential for every consumer (novox/hq ADR 0305, issue 526). // // **The value comes from the provider, never from whoever asks.** A module served by its machine's runtime calls // under the runtime's one bus account, which carries that machine's agents' calls too, so a value handed to a // verb could be anybody's. This line carries none: it asks the provider's own tool, on the provider's machine, in // the controller's own name — the subject that names its caller, which the bus lets the controller alone publish // (issue 365) — with a sealing key made for this one call. The tool answers only the controller, the value sealed // to that key; the controller opens it, seals it to every holder and forgets it, as the desk's value is (ADR 0277 // §23). Asking again fetches the same value again, and a value every holder already holds is not sealed anew. // // **What this trusts: the provider's machine.** The answer is taken from whoever answers the provider's tool // subject on the provider's machine first. On that machine its runtime serves the subject for every module it // carries, so anything that runtime runs, or anything else granted that subject there, could answer before the // provider does and hand the controller a value of its own. ADR 0305 accepts that by trusting the provider's // machine, as the provider itself is trusted; the bus keeps every other machine from answering it. // handActIssued is the verb a change of an issued value is recorded under: the mesh working as decided. const handActIssued = "secret issued" // issuedAsk is how long the provider's tool is given to answer. const issuedAsk = 20 * time.Second // issuedProvider is one assignment of a module whose offer says its software issues the provision's credential. type issuedProvider struct { Node, Module, Tool string } // issuedTaker is the line's work, with what it reaches handed in: the store, the bus and the sends. type issuedTaker struct { // providers are the assignments to ask, and whether any module offers the provision so at all. providers func(provision string) ([]issuedProvider, bool, error) ask func(p issuedProvider, args map[string]any) (link.Answer, error) keep func(provision, provider, value string) (inventory.IssuedKept, error) record func(act link.HandAct) error send func(machines []string) error } // take asks every provider of the provision for its software's value and keeps each; it answers what it did, in // words that never carry the value or its fingerprint. func (t issuedTaker) take(provision string) (string, error) { providers, offered, err := t.providers(provision) if err != nil { return "", err } if !offered { return "", fmt.Errorf("no module offers %q with a credential its software issues (\"credential\": "+ "{\"issued-by\": \"software\", \"tool\": …}, ADR 0305), so there is nothing to take from a provider; "+ "nothing was asked", provision) } if len(providers) == 0 { return fmt.Sprintf("no consumer holds %s from a provider whose software issues it yet, so there is nothing "+ "to seal; a consumer that binds holds a value the mesh made until the provider next asks", provision), nil } var said []string var failed []string for _, p := range providers { words, err := t.takeFrom(provision, p) if err != nil { failed = append(failed, fmt.Sprintf("%s on %s: %v", p.Module, p.Node, err)) continue } said = append(said, words) } out := strings.Join(said, "\n") if len(failed) > 0 { return out, fmt.Errorf("%s was not taken from every provider:\n %s", provision, strings.Join(failed, "\n ")) } return out, nil } // takeFrom is one provider: asked, opened, kept, recorded and sent when it changed. func (t issuedTaker) takeFrom(provision string, p issuedProvider) (string, error) { public, private, err := secrets.Keypair() if err != nil { return "", fmt.Errorf("no key could be made to take the value: %w", err) } answer, err := t.ask(p, map[string]any{"provision": provision, "seal_to": public}) if err != nil { return "", fmt.Errorf("its tool %s could not be asked: %w", p.Tool, err) } if answer.Error != "" { return "", fmt.Errorf("its tool %s refused: %s", p.Tool, answer.Error) } var given struct { Sealed string `json:"sealed"` } if err := unmarshalAnswer(answer, &given); err != nil || given.Sealed == "" { return "", fmt.Errorf("its tool %s answered no sealed value", p.Tool) } opened, err := secrets.Open(private, given.Sealed) if err != nil { // Never the value, never what failed to open: only that it was not sealed to this call. return "", fmt.Errorf("its tool %s answered a value not sealed to this call; nothing was taken", p.Tool) } value := strings.TrimSpace(string(opened)) for i := range opened { opened[i] = 0 } if value == "" { return "", fmt.Errorf("its tool %s answered an empty value; nothing was taken", p.Tool) } kept, err := t.keep(provision, p.Node, value) value = "" if err != nil { return "", err } holders := make([]string, 0, len(kept.Holders)) for _, h := range kept.Holders { holders = append(holders, fmt.Sprintf("%s on %s%s", h.ConsumerModule, h.Consumer, asLocal(h.Local))) } words := "" if len(kept.Unsealable) > 0 { var names []string for _, h := range kept.Unsealable { names = append(names, fmt.Sprintf("%s on %s", h.ConsumerModule, h.Consumer)) } words = fmt.Sprintf("\n not sealed to %s: no sealing key on that machine", strings.Join(names, ", ")) } if !kept.Changed { return fmt.Sprintf("%s from %s on %s: every holder already holds the value its software issued (%s); "+ "nothing was sent%s", provision, p.Module, p.Node, strings.Join(holders, ", "), words), nil } sealedTo := make([]string, 0, len(kept.Sealed)) for _, h := range kept.Sealed { sealedTo = append(sealedTo, fmt.Sprintf("%s on %s%s", h.ConsumerModule, h.Consumer, asLocal(h.Local))) } recorded := "" if err := t.record(link.HandAct{Verb: handActIssued, Args: []string{provision}, Why: fmt.Sprintf("%s on %s gave the credential its software issued for %s; sealed to %s", p.Module, p.Node, provision, strings.Join(sealedTo, ", ")), Cause: "issued-by-software"}); err != nil { recorded = fmt.Sprintf("\n this was NOT recorded in the hand-act log, and is done anyway: %v", err) } if err := t.send(kept.Machines); err != nil { return "", fmt.Errorf("%s from %s on %s is sealed to %s and could not be sent: %w; `push --behind` sends it", provision, p.Module, p.Node, strings.Join(sealedTo, ", "), err) } return fmt.Sprintf("%s from %s on %s: the value its software issued is sealed to %s; sent %s, and each of "+ "them starts again on it%s%s", provision, p.Module, p.Node, strings.Join(sealedTo, ", "), strings.Join(kept.Machines, ", "), words, recorded), nil } // issuedProvidersOf are the assignments to ask for a provision: every machine providing it to a pair the mesh // holds, and on it the assigned module whose offer says its software issues the credential (ADR 0305 §1). func issuedProvidersOf(ctx context.Context, inv *inventory.Inventory, provision string) ([]issuedProvider, bool, error) { shelf, err := inv.Catalogue(ctx) if err != nil { return nil, false, err } if !offersIssued(shelf, provision) { return nil, false, nil } holders, err := inv.HoldersOf(ctx, provision, "") if err != nil { return nil, true, err } nodes := map[string]bool{} for _, h := range holders { nodes[h.Provider] = true } names := make([]string, 0, len(nodes)) for n := range nodes { names = append(names, n) } sort.Strings(names) var out []issuedProvider for _, node := range names { assigned, err := inv.Assigned(ctx, node) if err != nil { return nil, true, err } out = append(out, issuedOn(shelf, node, assigned, provision)...) } return out, true, nil } // offersIssued is whether any module offers the provision with a credential its software issues. func offersIssued(shelf map[string]catalogue.Manifest, provision string) bool { for _, m := range shelf { if _, issued := m.IssuedCredentialOf(provision); issued { return true } } return false } // issuedOn are the modules assigned to one machine whose offer says their software issues the provision's // credential, each with the tool that answers it. func issuedOn(shelf map[string]catalogue.Manifest, node string, assigned []string, provision string) []issuedProvider { var out []issuedProvider for _, module := range assigned { if tool, issued := shelf[module].IssuedCredentialOf(provision); issued { out = append(out, issuedProvider{Node: node, Module: module, Tool: tool}) } } return out } // issuedRefusesRotation refuses `rotate` of a provision its provider's software issues (ADR 0305 §9): a value the // mesh made would replace one the software holds, and every consumer would hold a dead credential until the // provider next asks. func issuedRefusesRotation(shelf map[string]catalogue.Manifest, provision string) error { if !offersIssued(shelf, provision) { return nil } return fmt.Errorf("%s's credential is issued by its provider's software, which the mesh cannot make or "+ "rotate (ADR 0305): change it in that software, and `secret issued %s` takes the value it holds now. "+ "Nothing was rotated", provision, provision) } // secretIssued is `secret issued `, on this controller's stores and bus. func secretIssued(ctx context.Context, args []string) error { if len(args) != 1 || args[0] == "" || strings.HasPrefix(args[0], "-") { return errors.New("secret issued ") } provision := args[0] open, err := openStores(ctx) if err != nil { return err } defer open.Close() t := issuedTaker{ providers: func(provision string) ([]issuedProvider, bool, error) { return issuedProvidersOf(ctx, open.inventory, provision) }, ask: func(p issuedProvider, args map[string]any) (link.Answer, error) { var answer link.Answer err := onTheBus(func(conn *nats.Conn) error { var err error answer, err = link.AskModuleToolOnAs(ctx, conn, broker.ControllerName, p.Module, p.Tool, p.Node, args, issuedAsk) return err }) return answer, err }, keep: func(provision, provider, value string) (inventory.IssuedKept, error) { return open.inventory.KeepIssuedSecret(ctx, provision, provider, value) }, record: func(act link.HandAct) error { return onTheBus(func(conn *nats.Conn) error { _, err := link.RecordHandAct(ctx, conn, act) return err }) }, send: func(machines []string) error { return sendTo(ctx, open, machines) }, } words, err := t.take(provision) if words != "" { fmt.Println(words) } return err }