From 8bdfddab4aab5740b050e972b794061729c32520 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 09:49:14 +0100 Subject: [PATCH] Hand a module exactly when the vault's own answer judged it handed, so the node never composes a secret the vault leaves to the provisioner (issue 774) --- go.mod | 2 +- go.sum | 4 ++++ internal/selfcompose/selfcompose.go | 20 ++++++++++++++++++++ internal/selfcompose/selfcompose_test.go | 13 ++++++++++++- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 5f44beb..65a510a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index df73018..1700a57 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535 h1:Bgrm3YfWCxr+FEm03kEP5fDIe9hNbe+j0CDVatGAGKg= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031082917-2d1d8def7535/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031083427-de8e82c06f54 h1:xITIabv8ujmwpaEKBiQpkOx6U/ICLTTzX9WWC3DGOPQ= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031083427-de8e82c06f54/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a h1:DcSvfEmdPBKqCg5OmXRNb269EkkPe7BsEp+RykEhkgE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261031084708-acbf80ad910a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index c83bb8a..28d4867 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -19,6 +19,7 @@ import ( "encoding/json" "errors" "fmt" + "maps" "reflect" "slices" "sort" @@ -113,6 +114,10 @@ type Composer struct { // sealedOwn is each own secret's sealed value as first handed, by module/name and generation: a value sealed again // is other bytes, so the one first had is kept while the generation stands, and the composition stays the same. sealedOwn map[string]ownSealed + // ownHanded is, by module, whether the vault judged it handed to the provisioner in its last own answer (its + // `handed`), nil when that answer said nothing of it (a vault older than the field): the composition hands exactly + // those (frombus.HandedReader, issue 774). + ownHanded map[string]bool // certified is the serving key a certificate was last asked for successfully, so it is asked once per key. certified string said string @@ -1279,6 +1284,7 @@ func (r reader) OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecre Generation int `json:"generation"` Sealed json.RawMessage `json:"sealed"` } `json:"own"` + Handed map[string]bool `json:"handed"` } if err := json.Unmarshal(raw, &a); err != nil { return nil, fmt.Errorf("the vault's own answer is not one: %w", err) @@ -1288,6 +1294,7 @@ func (r reader) OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecre } r.c.mu.Lock() defer r.c.mu.Unlock() + r.c.ownHanded = a.Handed out := make([]compose.OwnSecretRow, 0, len(a.Own)) for _, o := range a.Own { // The box first had is kept while its generation stands, so sealing it again moves no composition; the box the @@ -1304,6 +1311,19 @@ func (r reader) OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecre return out, nil } +// Handed is, of this node's modules, which the vault judged handed to the provisioner when it last answered own +// (frombus.HandedReader, issue 774): the node hands exactly those, so the vault and the node judge a module one way. +func (r reader) Handed(_ context.Context, node string) map[string]bool { + if node != r.c.env.Node { + return nil + } + r.c.mu.Lock() + defer r.c.mu.Unlock() + return maps.Clone(r.c.ownHanded) +} + +var _ frombus.HandedReader = reader{} + func (r reader) Credentials(ctx context.Context, node string, asConsumer, asProvider []compose.CredentialKey) ([]compose.Credential, []compose.Issued, error) { if node != r.c.env.Node || r.c.env.Own == nil { return nil, nil, nil diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 2896afd..29a4a39 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "os" + "reflect" "slices" "strconv" "strings" @@ -163,7 +164,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { asks++ sealedAt[fmt.Sprintf(`{"ask":%d}`, asks)] = generation + 1 return json.RawMessage(fmt.Sprintf(`{"node":"n1","key":"sk","own":[{"module":"keeper","name":"password","origin":"made",`+ - `"generation":%d,"sealed":{"ask":%d}}]}`, generation+1, asks)), nil + `"generation":%d,"sealed":{"ask":%d}}],"handed":{"keeper":false}}`, generation+1, asks)), nil } secretChanged := make(chan []byte, 1) env.Hear = func(subject string) (<-chan []byte, func(), error) { @@ -767,6 +768,16 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { if rotated.Key != keeperKey { t.Fatalf("a new generation handed %s, want keeper again", rotated.Key) } + // Issue 774: what the vault judged handed in its own answer is what the node's composition hands, for n1 alone. + b.mu.Lock() + asked := asks + b.mu.Unlock() + if h := (reader{c: c}).Handed(ctx, "n1"); asked == 0 || !reflect.DeepEqual(h, map[string]bool{"keeper": false}) { + t.Errorf("the vault's handed verdicts as the composition reads them, after %d asks: %v", asked, h) + } + if h := (reader{c: c}).Handed(ctx, "n2"); h != nil { + t.Errorf("another node's verdicts: %v", h) + } // Review of L2 (round 2): rotated, then the node-engine restarts. The agent has applied nothing yet, so the box the // node runs comes from keeper's have (Held); the vault seals the value again, other bytes, and the box the have holds