diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 0736705..8246895 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -797,7 +797,7 @@ func selfComposer(ctx context.Context, session *link.Session, node string, say f Mesh: frombus.Mesh{InternalSuffix: overlay.Suffix()}, Say: say, // Its modules' own secrets, sealed by the vault to this machine's key (ADR 0550 §5). - Own: func(ctx context.Context) (json.RawMessage, error) { - return session.AskVault(ctx, "own", map[string]any{}) + Own: func(ctx context.Context, args map[string]any) (json.RawMessage, error) { + return session.AskVault(ctx, "own", args) }}) } diff --git a/go.mod b/go.mod index 02c9fd5..3e95fe5 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.20261026181356-4d5f43022453 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026181846-91413de33516 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 08e500b..2582f40 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026180300-f1d7c7377dc4 h1:rKDfp+IH git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026180300-f1d7c7377dc4/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026181356-4d5f43022453 h1:TemVEm3xcXHA1OWzrO0+cnuWsUpDUsnNE7yIlMaxMBA= git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026181356-4d5f43022453/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026181846-91413de33516 h1:xbbss6kQFrAiyy8UEvqh2Ddi2KP+O9oTUSh7GkqmCYU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026181846-91413de33516/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 47bd659..843ad70 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -39,9 +39,9 @@ type Env struct { Get func(ctx context.Context, bucket, key string) ([]byte, error) Mesh frombus.Mesh Say func(string) - // Own asks the vault's `own` in this node's engine's name: its modules' own secrets sealed to its key (ADR 0550 §5). - // Nil asks nothing, and a module with an own secret does not compose. - Own func(ctx context.Context) (json.RawMessage, error) + // Own asks the vault's `own` in this node's engine's name, with the arguments given: its modules' own secrets and the + // pair credentials asked, sealed to its key (ADR 0550 §5). Nil asks nothing, and a module with either does not compose. + Own func(ctx context.Context, args map[string]any) (json.RawMessage, error) } // Composer composes the node and is the agent's watcher of its compositions (agent.Env.Composed). @@ -511,7 +511,7 @@ func (r reader) OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecre if node != r.c.env.Node || r.c.env.Own == nil { return nil, nil } - raw, err := r.c.env.Own(ctx) + raw, err := r.c.env.Own(ctx, map[string]any{}) if err != nil { return nil, fmt.Errorf("the vault's own: %w", err) } @@ -547,3 +547,64 @@ func (r reader) OwnSecrets(ctx context.Context, node string) ([]compose.OwnSecre } return out, nil } + +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 + } + type asked struct { + Role string `json:"role"` + compose.CredentialKey + } + var ask []asked + for _, k := range asConsumer { + ask = append(ask, asked{Role: "consumer", CredentialKey: k}) + } + for _, k := range asProvider { + ask = append(ask, asked{Role: "provider", CredentialKey: k}) + } + raw, err := r.c.env.Own(ctx, map[string]any{"credentials": ask}) + if err != nil { + return nil, nil, fmt.Errorf("the vault's own: %w", err) + } + var a struct { + Node string `json:"node"` + Key string `json:"key"` + Credentials []struct { + Role string `json:"role"` + compose.CredentialKey + Name string `json:"name"` + Generation int `json:"generation"` + Sealed json.RawMessage `json:"sealed"` + } `json:"credentials"` + } + if err := json.Unmarshal(raw, &a); err != nil { + return nil, nil, fmt.Errorf("the vault's own answer is not one: %w", err) + } + if a.Node != node { + return nil, nil, fmt.Errorf("the vault answered %q's credentials to %s", a.Node, node) + } + r.c.mu.Lock() + defer r.c.mu.Unlock() + var creds []compose.Credential + var issued []compose.Issued + for _, c := range a.Credentials { + // The value first had is kept while its generation stands, so sealing it again moves no composition. + id := c.Role + "|" + c.Name + "|" + c.Provision + "|" + c.Consumer + "|" + c.ConsumerModule + "|" + c.Local + kept, ok := r.c.sealedOwn[id] + if !ok || kept.generation != c.Generation || kept.key != a.Key { + kept = ownSealed{generation: c.Generation, key: a.Key, sealed: string(c.Sealed)} + r.c.sealedOwn[id] = kept + } + k := c.CredentialKey + switch c.Role { + case "consumer": + creds = append(creds, compose.Credential{Provision: k.Provision, Consumer: k.Consumer, ConsumerModule: k.ConsumerModule, + Provider: k.Provider, Local: k.Local, State: compose.CredentialHeld, Sealed: kept.sealed}) + case "provider": + issued = append(issued, compose.Issued{Provision: k.Provision, Consumer: k.Consumer, ConsumerModule: k.ConsumerModule, + Local: k.Local, Provider: k.Provider, ForProvider: kept.sealed}) + } + } + return creds, issued, nil +} diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 44bba67..74a6d3a 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-sdk/go/compose" "git.novox.be/novox/mesh-sdk/go/delivery" "github.com/novox/mesh-host/internal/agent" @@ -89,7 +90,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { env := b.env() // The vault's own: keeper's password sealed to n1's key, sealed again (other bytes) at every ask. var asks, generation int - env.Own = func(context.Context) (json.RawMessage, error) { + env.Own = func(context.Context, map[string]any) (json.RawMessage, error) { b.mu.Lock() defer b.mu.Unlock() asks++ @@ -208,4 +209,29 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { if u := next(t, out); u.Key != keeperKey { t.Fatalf("a new generation handed %s, want keeper again", u.Key) } + + // The pair credentials asked (ADR 0550 §5): the keys go to the vault's own with their role, a consumer's comes back as + // a credential held, a provider's as the grant's copy, each sealed to n1's key, the value first had kept. + var sent map[string]any + pairEnv := b.env() + pairEnv.Own = func(_ context.Context, args map[string]any) (json.RawMessage, error) { + sent = args + return json.RawMessage(`{"node":"n1","key":"sk","credentials":[` + + `{"role":"consumer","provision":"pg","consumer":"n1","consumer_module":"web","provider":"n2","name":"pair/n2/pg/n1/web","generation":1,"sealed":{"c":1}},` + + `{"role":"provider","provision":"s3","consumer":"n3","consumer_module":"files","provider":"n1","name":"provision/n1/s3","generation":2,"sealed":{"p":1}}]}`), nil + } + pc := New(pairEnv) + consumer := []compose.CredentialKey{{Provision: "pg", Consumer: "n1", ConsumerModule: "web", Provider: "n2"}} + provider := []compose.CredentialKey{{Provision: "s3", Consumer: "n3", ConsumerModule: "files", Provider: "n1"}} + creds, issued, err := reader{c: pc}.Credentials(ctx, "n1", consumer, provider) + if err != nil || len(creds) != 1 || len(issued) != 1 { + t.Fatalf("credentials %+v issued %+v, %v", creds, issued, err) + } + if creds[0].State != compose.CredentialHeld || creds[0].Provider != "n2" || creds[0].Sealed != `{"c":1}` || + issued[0].Consumer != "n3" || issued[0].ForProvider != `{"p":1}` { + t.Errorf("credentials %+v issued %+v", creds, issued) + } + if asked, _ := json.Marshal(sent["credentials"]); !strings.Contains(string(asked), `"role":"consumer"`) || !strings.Contains(string(asked), `"role":"provider"`) { + t.Errorf("the vault was asked %s", asked) + } }