Files
mesh-host/cmd/mesh-host/provision_secrets.go
T
jschoubben cbe8528f91 Resolve ${secret:<name>} through this node's provision keys, and check the reference by the vault's own name (issue 1954 L6-E1, E2; ADR 0570 clauses 1, 6; ADR 0094)
- A placeholder is `${secret:<name>}`, the name the module calls the credential (catalogue.SecretLocal): the local
  name where it keeps several values of one provision, else the provision. The `<provision>.<local>` form goes.
- The engine finds the one key under `<node>.<module>.` of mesh-provisioner's `provisions` going by that name. That is
  what node.<node> may read, by a watch to its caught-up mark; plans are not read.
- The reference's ID must be a credential name of that provision (provisions.VaultName). The vault's answer must carry
  exactly that name and generation. Anything else refuses the write, naming both.

Pins mesh-sdk 66ff33d. Tests: rows in TestAWriteAndAnApplyOfTheSameFileSerialise (a local name, a name no provision
goes by, a reference to no credential of the provision, a mismatched answer).
2026-10-29 22:41:44 +01:00

169 lines
6.9 KiB
Go

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:<provision>}`, 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:<name>}` (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 `<node>.<consumer>.<provision>[.<local>]` 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 (`<node>.<consumer>.`), by key: what this
// engine may read, its own node's (`<node>.>`).
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:<name>}`.
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)
}