package main import ( "context" "encoding/json" "fmt" "regexp" "sort" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/provisions" ) // provisionSecrets fills a provision's value into a file the provisioner's client hands the node-engine to write // (novox/hq ADR 0570 clause 6): the client fills the file but for each `${secret:}`, and the node-engine // fills those at the write, so the client never holds the value. A whole `secrets` file of a handed consumer is the // placeholder alone. // // A placeholder names the credential as the module calls it, `${secret:}` (catalogue.SecretLocal, ADR 0094): the // local name where the module keeps several values of one provision, else the provision. For each, it finds the one // provision key `..[.]` of the seat mesh-provisioner's state `provisions` (clause 1) // going by that name, which must be `provided` and carry the vault's reference; asks the vault for that pair // credential sealed to this machine's key (the verb `own`, ADR 0349 §11 with the node-engine standing for the client, // ADR 0550 §5); and opens it with this machine's sealing key. Anything short of that refuses the write, naming the // provision and why: a file written with a placeholder left in it, or with no value, is a module that starts and cannot // connect. type provisionSecrets struct { node string // list is every key of mesh-provisioner's `provisions` under prefix (`..`), by key: what this // engine may read, its own node's (`.>`). list func(ctx context.Context, prefix string) (map[string][]byte, error) // own asks the vault's verb `own` in this engine's name. own func(ctx context.Context, args map[string]any) (json.RawMessage, error) // open opens a value sealed to this machine. open func(sealed string) ([]byte, error) } // secretPlaceholder is `${secret:}`. var secretPlaceholder = regexp.MustCompile(`\$\{secret:([A-Za-z0-9_-]+)\}`) // fill is content with each provision placeholder of module's filled, and whether any was. func (p provisionSecrets) fill(ctx context.Context, module string, content []byte) ([]byte, bool, error) { found := secretPlaceholder.FindAllSubmatch(content, -1) if len(found) == 0 { return content, false, nil } if module == "" { return nil, false, fmt.Errorf("the file names %d provision secret(s) and belongs to no module, so whose they are is not known", len(found)) } if p.list == nil || p.own == nil || p.open == nil { return nil, false, fmt.Errorf("%s's provisions cannot be read on this machine: the node-engine is not linked to the mesh", module) } prefix := p.node + "." + module + "." keys, err := p.list(ctx, prefix) if err != nil { return nil, false, fmt.Errorf("%s's provisions cannot be read: %w", module, err) } out := string(content) done := map[string]bool{} for _, f := range found { whole, name := string(f[0]), string(f[1]) if done[whole] { continue } done[whole] = true pr, key, err := byName(keys, name) if err != nil { return nil, false, fmt.Errorf("%s's secret %q: %w", module, name, err) } value, err := p.value(ctx, key, pr) if err != nil { return nil, false, err } out = strings.ReplaceAll(out, whole, string(value)) } return []byte(out), true, nil } // byName is the one provision of keys the module calls name (catalogue.SecretLocal), and its key. func byName(keys map[string][]byte, name string) (provisions.Provision, string, error) { var hit []string var got provisions.Provision for key, raw := range keys { pr, err := provisions.Decode(raw) if err != nil { return provisions.Provision{}, "", fmt.Errorf("the provision %s: %w", key, err) } if catalogue.SecretLocal(pr.Provision, pr.Local) == name { hit, got = append(hit, key), pr } } switch len(hit) { case 0: return provisions.Provision{}, "", fmt.Errorf("no provision of %s goes by that name: nothing asks for it", provisions.ProvisionsRead) case 1: return got, hit[0], nil } sort.Strings(hit) return provisions.Provision{}, "", fmt.Errorf("the provisions %s all go by that name", strings.Join(hit, ", ")) } // value is one provision's value on this node, opened. func (p provisionSecrets) value(ctx context.Context, key string, pr provisions.Provision) ([]byte, error) { if pr.State != provisions.Provided || pr.Reference == nil { why := pr.State if pr.Why != "" { why += ": " + pr.Why } if pr.State == provisions.Provided { why = "provided with no reference to the vault's value" } return nil, fmt.Errorf("the provision %s is %s, so its value is not written yet", key, why) } // The reference names a credential of this provision, as the vault names it (provisions.VaultName). ours := false for _, form := range []string{provisions.FormPair, provisions.FormShared, provisions.FormIssued} { ours = ours || provisions.VaultName(form, pr) == pr.Reference.ID } if !ours { return nil, fmt.Errorf("the provision %s names the vault's %s, which is no credential of it", key, pr.Reference.ID) } module, provision, local := pr.Consumer.Module, pr.Provision, pr.Local type asked 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"` } ask := asked{Role: "consumer", Provision: provision, Consumer: p.node, ConsumerModule: module, Provider: pr.Provider.Node, Local: local} answer, err := p.own(ctx, map[string]any{"credentials": []asked{ask}}) if err != nil { return nil, fmt.Errorf("the vault's value of %s: %w", key, err) } var a struct { Node string `json:"node"` Credentials []struct { asked Name string `json:"name"` Generation int `json:"generation"` Sealed json.RawMessage `json:"sealed"` } `json:"credentials"` } if err := json.Unmarshal(answer, &a); err != nil { return nil, fmt.Errorf("the vault's answer for %s is not one: %w", key, err) } if a.Node != p.node { return nil, fmt.Errorf("the vault answered %s's credentials for %s on %s", a.Node, key, p.node) } for _, c := range a.Credentials { if c.asked != ask { continue } // The value the provision names, by the vault's credential name and its generation (clause 6): another is refused. if c.Name != pr.Reference.ID || c.Generation != pr.Reference.Generation { return nil, fmt.Errorf("the vault answered %s generation %d for %s, and the provision names %s generation %d: "+ "nothing is written until they agree", c.Name, c.Generation, key, pr.Reference.ID, pr.Reference.Generation) } value, err := p.open(string(c.Sealed)) if err != nil { return nil, fmt.Errorf("the vault's value of %s does not open on this machine: %w", key, err) } return value, nil } return nil, fmt.Errorf("the vault answered no value of %s for this machine", key) }