Pins mesh-sdk 3028c7e (delivery.OwnNeedName). The fixtures carry the id shape captured from novox's shadow answer.
297 lines
11 KiB
Go
297 lines
11 KiB
Go
// Package ownresync hands the vault the value of this machine's own secret that runs, where the vault holds another
|
|
// (novox/hq issue 1126, cause C4).
|
|
//
|
|
// The vault's `move-in` never replaces a value it holds, so its first import of an own secret stays fixed; where the
|
|
// controller later made that secret anew (a rotation at start, a re-issue), the new value runs and the vault keeps the
|
|
// old one. The node's shadow composer sees it: composing from the vault's value, it opens both seals with this
|
|
// machine's key and its answer's shown body says delivery.MaskedAnotherValue. After each shadow answer this step asks the
|
|
// vault's `move-in` with replace_running: true and what runs, for each own secret so masked, once per running value, and
|
|
// says what it asked and what the vault answered. It is move-in's, the verb this node may ask already, so no seat and no
|
|
// bus grant changes for it.
|
|
//
|
|
// **Only while this node applies the controller's compositions**, and only for a module whose composition that runs is
|
|
// the one its want names: then the controller's value is what runs, and what runs is the truth the vault lacks. Once the
|
|
// node composes its own, the vault's value is the source and nothing is handed. **The switch** is the vault's
|
|
// (`own_take_switch`, a person's act): while it is off the vault refuses every ask, and this step says so once per value.
|
|
// The vault also refuses any value it changed in the last 15 minutes, so a re-issue the node has not applied yet is
|
|
// never put back.
|
|
package ownresync
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Verb is the vault's verb this step asks, with ReplaceRunning true in both steps.
|
|
const Verb = "move-in"
|
|
|
|
// ReplaceRunning is move-in's argument that makes it take the running value over the one the vault holds.
|
|
const ReplaceRunning = "replace_running"
|
|
|
|
// Env is what the step reads and asks.
|
|
type Env struct {
|
|
Node string
|
|
// Composes is whose compositions the node applies now: delivery.ComposesController, delivery.ComposesOwn, or ""
|
|
// while its agent has not decided. The step hands nothing but for ComposesController.
|
|
Composes func() string
|
|
// Running is what the node runs now (agent.Everything).
|
|
Running func() []delivery.Composed
|
|
// Get reads one key's latest value; nil and no error when there is none.
|
|
Get func(ctx context.Context, bucket, key string) ([]byte, error)
|
|
// Unseal opens a value sealed to this machine; Seal seals a value to a raw X25519 key in base64 (identity.Seal).
|
|
Unseal func(sealed string) ([]byte, error)
|
|
Seal func(to string, value []byte) (string, error)
|
|
// Ask asks one of the vault's verbs in this engine's own name (link.Session.AskVault).
|
|
Ask func(ctx context.Context, verb string, args map[string]any) (json.RawMessage, error)
|
|
Say func(string)
|
|
}
|
|
|
|
// Outcome is what the step did for one own secret: asked or not, and what was said.
|
|
type Outcome struct {
|
|
Module string
|
|
Name string
|
|
Asked bool
|
|
Said string
|
|
}
|
|
|
|
// Resync is the step of one node agent's run.
|
|
type Resync struct {
|
|
env Env
|
|
|
|
mu sync.Mutex
|
|
// done is, by module/name, the running value's fingerprint the vault was asked with or why it was not: each is
|
|
// asked, and each reason said, once.
|
|
done map[string]string
|
|
}
|
|
|
|
// New is the step for one node.
|
|
func New(env Env) *Resync {
|
|
if env.Say == nil {
|
|
env.Say = func(string) {}
|
|
}
|
|
return &Resync{env: env, done: map[string]string{}}
|
|
}
|
|
|
|
// Take reads the node's shadow answers and hands the vault what runs of each own secret they say another value of.
|
|
func (r *Resync) Take(ctx context.Context, answers map[string]delivery.CandidateAnswer) []Outcome {
|
|
seen := map[delivery.OwnValueDiffers]bool{}
|
|
var drift []delivery.OwnValueDiffers
|
|
groups := make([]string, 0, len(answers))
|
|
for g := range answers {
|
|
groups = append(groups, g)
|
|
}
|
|
sort.Strings(groups)
|
|
for _, g := range groups {
|
|
for _, o := range delivery.OwnValuesDiffer(answers[g]) {
|
|
if !seen[o] {
|
|
seen[o] = true
|
|
drift = append(drift, o)
|
|
}
|
|
}
|
|
}
|
|
if len(drift) == 0 {
|
|
return nil
|
|
}
|
|
composes := ""
|
|
if r.env.Composes != nil {
|
|
composes = r.env.Composes()
|
|
}
|
|
var out []Outcome
|
|
for _, o := range drift {
|
|
res := r.one(ctx, composes, o)
|
|
if res.Said != "" {
|
|
out = append(out, res)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// one is the step for one own secret: Said is empty when it was done before for the same value and reason.
|
|
func (r *Resync) one(ctx context.Context, composes string, o delivery.OwnValueDiffers) Outcome {
|
|
res := Outcome{Module: o.Module, Name: o.Name}
|
|
key := o.Module + "/" + o.Name
|
|
if composes == "" {
|
|
return res // the agent has not decided whose compositions it applies: taken again once it has
|
|
}
|
|
if composes != delivery.ComposesController {
|
|
return r.said(key, "not:composes", res, fmt.Sprintf("%s holds another value in the vault than runs here, and "+
|
|
"nothing is handed: this node composes its own (%q), so the vault's value is the source", key, composes))
|
|
}
|
|
sealed, why := r.running(ctx, o)
|
|
if why != "" {
|
|
return r.said(key, "not:"+why, res, fmt.Sprintf("%s holds another value in the vault than runs here, and "+
|
|
"nothing is handed: %s", key, why))
|
|
}
|
|
sum := sha256.Sum256([]byte(sealed))
|
|
fp := hex.EncodeToString(sum[:])
|
|
r.mu.Lock()
|
|
asked := r.done[key] == fp
|
|
r.mu.Unlock()
|
|
if asked {
|
|
return res
|
|
}
|
|
value, err := r.env.Unseal(sealed)
|
|
if err != nil {
|
|
return r.said(key, "not:unseal:"+fp, res, fmt.Sprintf("%s: what runs does not open with this machine's key, so "+
|
|
"nothing is handed: %v", key, err))
|
|
}
|
|
defer wipe(value)
|
|
said, err := r.hand(ctx, o, value)
|
|
if err != nil {
|
|
if ctx.Err() != nil {
|
|
return res // the run ended: asked again in the next
|
|
}
|
|
said = fmt.Sprintf("%s: the vault's %s with replace_running refused or did not answer: %v", key, Verb, err)
|
|
}
|
|
// Once per running value, whatever the vault answered: a refusal is the vault's word, said once, and a new value
|
|
// that runs asks again.
|
|
r.mu.Lock()
|
|
r.done[key] = fp
|
|
r.mu.Unlock()
|
|
res.Asked, res.Said = true, said
|
|
r.env.Say(said)
|
|
return res
|
|
}
|
|
|
|
// said says why once per reason, and keeps the reason as done.
|
|
func (r *Resync) said(key, reason string, res Outcome, why string) Outcome {
|
|
r.mu.Lock()
|
|
again := r.done[key] == reason
|
|
r.done[key] = reason
|
|
r.mu.Unlock()
|
|
if again {
|
|
return res
|
|
}
|
|
res.Said = why
|
|
r.env.Say(why)
|
|
return res
|
|
}
|
|
|
|
// running is the sealed value of o in the composition that runs, when that composition is the one the module's want
|
|
// names; or why it is not handed.
|
|
func (r *Resync) running(ctx context.Context, o delivery.OwnValueDiffers) (string, string) {
|
|
var comp *delivery.Composed
|
|
if r.env.Running != nil {
|
|
for _, c := range r.env.Running() {
|
|
if c.Module == o.Module {
|
|
c := c
|
|
comp = &c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if comp == nil {
|
|
return "", o.Module + " runs no composition here"
|
|
}
|
|
wantKey, err := delivery.WantKey(r.env.Node, o.Module)
|
|
if err != nil {
|
|
return "", err.Error()
|
|
}
|
|
raw, err := r.env.Get(ctx, delivery.AssignerWantsBucket, wantKey)
|
|
if err != nil || raw == nil {
|
|
return "", fmt.Sprintf("its want %s could not be read (%v)", wantKey, err)
|
|
}
|
|
want, err := delivery.DecodeWant(raw)
|
|
if err != nil {
|
|
return "", fmt.Sprintf("its want %s is not one: %v", wantKey, err)
|
|
}
|
|
if want.Self || want.Removed || want.Composed == "" {
|
|
return "", fmt.Sprintf("its want %s names no composition of the controller's", wantKey)
|
|
}
|
|
// What runs is the composition the want names, as the module's have says: applied and running.
|
|
haveKey, err := delivery.HaveKey(r.env.Node, o.Module)
|
|
if err != nil {
|
|
return "", err.Error()
|
|
}
|
|
raw, err = r.env.Get(ctx, delivery.HaveBucket, haveKey)
|
|
if err != nil || raw == nil {
|
|
return "", fmt.Sprintf("its have %s could not be read (%v)", haveKey, err)
|
|
}
|
|
have, err := delivery.DecodeHave(raw)
|
|
if err != nil {
|
|
return "", fmt.Sprintf("its have %s is not one: %v", haveKey, err)
|
|
}
|
|
if have.Composed != want.Composed || have.Phase != delivery.PhaseRunning {
|
|
return "", fmt.Sprintf("what runs (%s, %s) is not yet the composition its want names (%s)", have.Composed,
|
|
have.Phase, want.Composed)
|
|
}
|
|
// On a node the resource's id carries its module's name (`only-office.needs-jwt`); delivery.OwnNeedName reads it.
|
|
id := o.Module + "." + catalogue.NeedID(o.Name)
|
|
for _, res := range comp.Resources {
|
|
var body struct {
|
|
ID string `json:"id"`
|
|
Sealed any `json:"sealed"`
|
|
}
|
|
if json.Unmarshal(res, &body) != nil {
|
|
continue
|
|
}
|
|
if name, own := delivery.OwnNeedName(o.Module, body.ID); !own || name != o.Name {
|
|
continue
|
|
}
|
|
if s, ok := body.Sealed.(string); ok && s != "" {
|
|
return s, ""
|
|
}
|
|
return "", fmt.Sprintf("its resource %s carries no value sealed to this machine", id)
|
|
}
|
|
return "", fmt.Sprintf("what runs holds no resource %s", id)
|
|
}
|
|
|
|
// hand asks the vault's two steps with value, and checks what the vault holds after by its fingerprint.
|
|
func (r *Resync) hand(ctx context.Context, o delivery.OwnValueDiffers, value []byte) (string, error) {
|
|
raw, err := r.env.Ask(ctx, Verb, map[string]any{ReplaceRunning: true, "module": o.Module, "name": o.Name,
|
|
"why": "this node's shadow answer says the vault holds another value than runs here"})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var opened struct {
|
|
Call string `json:"call"`
|
|
SealTo string `json:"seal_to"`
|
|
Secret string `json:"secret"`
|
|
}
|
|
if err := json.Unmarshal(raw, &opened); err != nil || opened.Call == "" || opened.SealTo == "" {
|
|
return "", fmt.Errorf("the vault answered no call and no key to seal to")
|
|
}
|
|
sealed, err := r.env.Seal(opened.SealTo, value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("the vault's key for the call is not a sealing key: %w", err)
|
|
}
|
|
raw, err = r.env.Ask(ctx, Verb, map[string]any{ReplaceRunning: true, "call": opened.Call, "sealed": sealed})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var kept struct {
|
|
Secret string `json:"secret"`
|
|
From int `json:"from_generation"`
|
|
To int `json:"to_generation"`
|
|
Unchanged bool `json:"unchanged"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
Said string `json:"said"`
|
|
}
|
|
if err := json.Unmarshal(raw, &kept); err != nil || kept.Secret == "" {
|
|
return "", fmt.Errorf("the vault answered %s with no secret", opened.Secret)
|
|
}
|
|
sum := sha256.Sum256(value)
|
|
if kept.Fingerprint != "sha256:"+hex.EncodeToString(sum[:]) {
|
|
return "", fmt.Errorf("the vault says it holds %s at generation %d, but not the value handed in", kept.Secret, kept.To)
|
|
}
|
|
if kept.Unchanged {
|
|
return fmt.Sprintf("handed the vault what runs of %s: it held that value already (generation %d)", kept.Secret, kept.To), nil
|
|
}
|
|
return fmt.Sprintf("handed the vault what runs of %s: generation %d replaced by %d, the value that runs", kept.Secret,
|
|
kept.From, kept.To), nil
|
|
}
|
|
|
|
func wipe(b []byte) {
|
|
for i := range b {
|
|
b[i] = 0
|
|
}
|
|
}
|