From bd9ff3797d3a1cd0eec684219233daca715e48e2 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 06:42:55 +0000 Subject: [PATCH] Test the credential half of the rendering over an Input (issue 1364). Fixtures from mesh-control 6c2a8d62 outside_not_given_test.go (the assistant's two secrets) and the facts_test.go provider and consumer; the refusals checked word for word against plan.go and secrets.go. Written after the code, each rule broken in turn and a test seen to fail. The bus user list is an Input value, so its test is that it is carried (see the commit of the code). Unit level 1.04 s of 2 s. --- go/compose/credentials_test.go | 367 +++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 go/compose/credentials_test.go diff --git a/go/compose/credentials_test.go b/go/compose/credentials_test.go new file mode 100644 index 0000000..2db33ed --- /dev/null +++ b/go/compose/credentials_test.go @@ -0,0 +1,367 @@ +package compose + +import ( + "encoding/json" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/overlay" +) + +// secretValue is a value the Input carries that must never appear in anything the composition says. +const secretValue = "SECRET-VALUE-THAT-MUST-NOT-LEAK" + +func rendered(t *testing.T, in Input, node string, mode Mode) Rendered { + t.Helper() + got, err := Render(in, node, mode) + if err != nil { + t.Fatalf("%v", err) + } + return got +} + +// anAssistant is mesh-controller's outside_not_given_test.go anAssistant (cmd/mesh-controller at 6c2a8d62): a module with a secret +// the mesh makes and one only an outside party issues. +func anAssistant() catalogue.Manifest { + return catalogue.Manifest{Module: "assistant", Version: "1", + OwnSecrets: catalogue.OwnSecrets{ + "password": {Path: "/var/lib/mesh/assistant/password", Taken: catalogue.TakenAtStart}, + "vendor-key": {Path: "/var/lib/mesh/assistant/vendor-key", Taken: catalogue.TakenAtStart, IssuedBy: catalogue.IssuedOutside}, + }, + Resources: []map[string]any{{"id": "state", "type": "directory", "path": "/var/lib/mesh/assistant", "mode": "0700"}}} +} + +func withAssistant(t *testing.T) Input { + t.Helper() + in := aMesh(t) + in.Catalogue["assistant"] = anAssistant() + laptop := nodeOf(t, &in, "laptop") + laptop.Assigned = append(laptop.Assigned, "assistant") + laptop.SealingKey = keyOf(t, "laptop") + return in +} + +// Own secrets (plan.go renderingFor, novox/hq issue 275 and 401): one held and sealed to the machine's key is carried; one not held is +// foreseen in the Foreseeing mode and left out in the Reading mode, and nothing is made. +func TestAnOwnSecretHeldIsCarriedAndOneNotHeldIsForeseenOrLeftOut(t *testing.T) { + mk := func() Input { + in := withAssistant(t) + in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", + SealedTo: nodeOf(t, &in, "laptop").SealingKey, Sealed: secretValue}} + return in + } + in := mk() + got := rendered(t, in, "laptop", Foreseeing) + if got.Rendering.Needed["assistant"]["password"] != secretValue || len(got.Rendering.Foreseen) != 0 { + t.Errorf("a held secret: needed %v foreseen %v", got.Rendering.Needed, got.Rendering.Foreseen) + } + // Not held: foreseen with a stand-in (Foreseeing), left out (Reading). + in.OwnSecrets = nil + got = rendered(t, in, "laptop", Foreseeing) + if !got.Rendering.Foreseen["assistant"]["password"] || got.Rendering.Needed["assistant"]["password"] != "" || + len(got.Foreseen) != 1 || got.Foreseen[0] != "assistant/password" { + t.Errorf("a secret the send makes: foreseen %v / %v needed %v", got.Rendering.Foreseen, got.Foreseen, got.Rendering.Needed) + } + got = rendered(t, in, "laptop", Reading) + if len(got.Rendering.Needed["assistant"]) != 0 || len(got.Foreseen) != 0 || got.Rendering.Foreseen != nil { + t.Errorf("Reading composed a secret that was never issued: %v %v", got.Rendering.Needed, got.Foreseen) + } + // Held but sealed to a key the machine no longer has is not held. + in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: "an-old-key", Sealed: secretValue}} + if got := rendered(t, in, "laptop", Foreseeing); !got.Rendering.Foreseen["assistant"]["password"] { + t.Errorf("a secret sealed to an old key is held: %v", got.Rendering.Needed) + } + // Held with no value in the Input: a stand-in, named. + in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: nodeOf(t, &in, "laptop").SealingKey}} + if got := rendered(t, in, "laptop", Reading); !got.Rendering.Foreseen["assistant"]["password"] { + t.Errorf("a held secret with no value is not named: %v", got.Rendering.Foreseen) + } + unchanged(t, mk, func(in Input) { rendered(t, in, "laptop", Foreseeing) }) +} + +// An own secret issued outside the mesh that nobody gave is never made (novox/hq issue 401, ADR 0228): nothing is composed or +// foreseen for it; a value the mesh made for it is not one a person gave; a value given is placed once it is sealed to the key held now. +func TestAnOutsideSecretIsPlacedOnlyOnceAPersonGaveIt(t *testing.T) { + in := withAssistant(t) + key := nodeOf(t, &in, "laptop").SealingKey + for _, mode := range []Mode{Reading, Foreseeing} { + got := rendered(t, in, "laptop", mode) + if _, there := got.Rendering.Needed["assistant"]["vendor-key"]; there || got.Rendering.Foreseen["assistant"]["vendor-key"] { + t.Fatalf("%v: a secret nobody gave is composed: %v %v", mode, got.Rendering.Needed, got.Rendering.Foreseen) + } + } + in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "vendor-key", Origin: "made", SealedTo: key, Sealed: secretValue}} + if _, there := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; there { + t.Error("a value the mesh made for a secret issued outside was placed") + } + in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "vendor-key", Origin: "accepted", SealedTo: key, Sealed: "the vendor's key, sealed"}} + if got := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; got != "the vendor's key, sealed" { + t.Errorf("a value given is placed as %q", got) + } + in.OwnSecrets[0].SealedTo = "an-old-key" + if _, there := rendered(t, in, "laptop", Foreseeing).Rendering.Needed["assistant"]["vendor-key"]; there { + t.Error("a value given under an old key was placed") + } +} + +// 1364/AC3 (the send's refusals, novox/hq issue 203 and 275): in the Foreseeing mode a bus credential nobody issued and a given secret +// under an old key are refused in the controller's words; with the credential issued, the secret is foreseen. No secret value is in +// an error. +func TestTheSendsRefusalsAreRefusedInTheSendsWords(t *testing.T) { + broker := catalogue.Manifest{Module: "gate", Version: "1", + OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/gate/broker", Taken: catalogue.TakenAtStart}}} + in := withAssistant(t) + in.Catalogue["gate"] = broker + laptop := nodeOf(t, &in, "laptop") + laptop.Assigned = append(laptop.Assigned, "gate") + _, err := Render(in, "laptop", Foreseeing) + want := "gate on laptop has no bus credential: nothing was issued for laptop.gate, and a push would seal a placeholder " + + "its process cannot read (novox/hq issue 203). `module issue gate --node laptop`, then push again" + if err == nil || err.Error() != want { + t.Fatalf("got %v, want %q", err, want) + } + // Reading refuses nothing for it: the machine is not running it. + if _, err := Render(in, "laptop", Reading); err != nil { + t.Fatalf("Reading refused a secret the machine is not running: %v", err) + } + in.BusMinted = []string{"laptop.gate"} + if got := rendered(t, in, "laptop", Foreseeing); !got.Rendering.Foreseen["gate"]["broker"] { + t.Errorf("an issued bus credential is not foreseen: %v", got.Rendering.Foreseen) + } + // A given secret under an old key. + in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "accepted", SealedTo: "an-old-key", Sealed: secretValue}, + {Node: "laptop", Module: "gate", Name: "broker", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}} + _, err = Render(in, "laptop", Foreseeing) + wantOld := "assistant on laptop holds \"password\", which was given to the mesh rather than made by it, and laptop has since generated a new " + + "sealing key. The mesh cannot make another; issue it again" + if err == nil || err.Error() != wantOld { + t.Fatalf("got %v, want %q", err, wantOld) + } + if strings.Contains(err.Error(), secretValue) { + t.Error("a secret value is in an error") + } + // A machine with no sealing key cannot be sealed to. + laptop.SealingKey = "" + in.OwnSecrets = nil + if _, err := Render(in, "laptop", Foreseeing); err == nil || !strings.Contains(err.Error(), "laptop has no sealing key, so nothing can be sealed to it") { + t.Fatalf("no sealing key: %v", err) + } +} + +// aGrantMesh is the objects provider on anchor and its consumer files on laptop, with the pair credential made. +func aGrantMesh(t *testing.T) Input { + t.Helper() + in := theObjectsMesh(t, objects()) + in.Issued = []Issued{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", ForProvider: "sealed-to-anchor"}} + return in +} + +// A credential made for a consumer is a grant on its provider's machine, carrying the consumer's values and the sealed copy; without +// the sealed copy a stand-in is composed and named (novox/hq plan.go grantsFor). +func TestAGrantIsCreatedForEachCredentialMadeForAConsumer(t *testing.T) { + in := aGrantMesh(t) + got := rendered(t, in, "anchor", Foreseeing) + if len(got.Rendering.Grants) != 1 { + t.Fatalf("grants are %+v", got.Rendering.Grants) + } + g := got.Rendering.Grants[0] + if g.Provision != "s3-bucket" || g.Consumer != "laptop" || g.From != "files" || g.At != "laptop.internal" || g.Sealed != "sealed-to-anchor" { + t.Errorf("the grant is %+v", g) + } + if len(got.Foreseen) != 0 || len(got.Withheld) != 0 || len(got.Unbound) != 0 { + t.Errorf("foreseen %v withheld %v unbound %v", got.Foreseen, got.Withheld, got.Unbound) + } + in.Issued[0].ForProvider = "" + got = rendered(t, in, "anchor", Foreseeing) + if got.Rendering.Grants[0].Sealed != catalogue.ForeseenSealed || len(got.Foreseen) != 1 { + t.Errorf("a grant with no sealed copy: %+v foreseen %v", got.Rendering.Grants, got.Foreseen) + } + // A credential made for a consumer on another provider is not this node's grant. + if len(rendered(t, in, "laptop", Foreseeing).Rendering.Grants) != 0 { + t.Error("the consumer is granted its own credential") + } + // A consumer whose set does not resolve is skipped, not fatal. + nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "files") + in.Catalogue["files"] = catalogue.Manifest{Module: "files", Version: "1", Requires: []string{"s3-bucket"}, + Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}} + in.Catalogue["twin"] = catalogue.Manifest{Module: "twin", Version: "1", Claims: []catalogue.Claim{{Name: "x", Scope: catalogue.ScopeNode}}} + nodeOf(t, &in, "laptop").Assigned = append(nodeOf(t, &in, "laptop").Assigned, "twin") + if got, err := Render(in, "anchor", Foreseeing); err != nil || len(got.Rendering.Grants) != 0 { + t.Errorf("a consumer that does not resolve: %v %+v", err, got.Rendering.Grants) + } +} + +// 1364/AC2: a consumer the grants leave out because its identity overflows the provision's bound is returned beside them, naming the +// consumer and the provision (novox/hq ADR 0225), and is not granted. +func TestAConsumerWhoseIdentityOverflowsIsLeftOutOfTheGrantsAndNamed(t *testing.T) { + in := aGrantMesh(t) + in.Nodes[1].Name = "a-machine-with-a-very-long-name" + in.Issued[0].Consumer = "a-machine-with-a-very-long-name" + got := rendered(t, in, "anchor", Foreseeing) + if len(got.Rendering.Grants) != 0 || len(got.Withheld) != 1 { + t.Fatalf("grants %+v withheld %+v", got.Rendering.Grants, got.Withheld) + } + w := got.Withheld[0] + if w.Consumer != "a-machine-with-a-very-long-name" || w.Provision != "s3-bucket" || w.Provider != "anchor" || w.Module != "files" || + !strings.Contains(w.String(), "a-machine-with-a-very-long-name") || !strings.Contains(w.String(), "s3-bucket") { + t.Errorf("the consumer left out is %+v / %s", w, w.String()) + } + if len(got.Rendering.Withheld) != 1 { + t.Errorf("the Rendering does not carry it: %+v", got.Rendering.Withheld) + } +} + +// 1364/AC3: a credential on record for a consumer that its own resolution binds to another provider is withdrawn, named with where it is +// bound (novox/hq issue 274): the grant stays, with no consumer module asking. +func TestACredentialBoundToAnotherProviderIsWithdrawnAndSaidWhereItIsBound(t *testing.T) { + in := aGrantMesh(t) + second := objects() + second.Module = "objects2" + in.Catalogue["objects2"] = second + in.Nodes = append(in.Nodes, Node{Name: "ace", Place: Place{Site: "there", Address: "10.77.0.3"}, + Assigned: []string{overlay.Name, "objects2"}, Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}}) + nodeOf(t, &in, "laptop").Pins = map[string]catalogue.Chosen{"s3-bucket": {Node: "ace", Module: "objects2"}} + got := rendered(t, in, "anchor", Foreseeing) + if len(got.Unbound) != 1 { + t.Fatalf("unbound %+v grants %+v", got.Unbound, got.Rendering.Grants) + } + u := got.Unbound[0] + if u.Provision != "s3-bucket" || u.Provider != "anchor" || u.Consumer != "laptop" || u.Module != "files" || + len(u.BoundTo) != 1 || u.BoundTo[0] != "ace" || !strings.Contains(u.String(), "ace") { + t.Errorf("unbound is %+v / %s", u, u.String()) + } + if len(got.Rendering.Grants) != 1 || got.Rendering.Grants[0].From != "" { + t.Errorf("the withdrawn credential is still asked for: %+v", got.Rendering.Grants) + } +} + +func aCertificateMesh(t *testing.T) Input { + t.Helper() + in := aMesh(t) + in.Catalogue["secure"] = catalogue.Manifest{Module: "secure", Version: "1", Certificate: &catalogue.Certificate{Into: "/etc/secure/tls.pem", Authority: "/etc/secure/ca.pem"}} + laptop := nodeOf(t, &in, "laptop") + laptop.Assigned = append(laptop.Assigned, "secure") + return in +} + +// 1364/AC4: with no certificate and authority in the Input a stand-in is composed and named; given them, the composition carries them as +// given. A machine that never reported a serving key, and one not on the private network, are refused in the controller's words. +func TestACertificateIsCarriedAsGivenOrStoodInForAndNamed(t *testing.T) { + in := aCertificateMesh(t) + laptop := nodeOf(t, &in, "laptop") + if _, err := Render(in, "laptop", Foreseeing); err == nil || err.Error() != "laptop wants a certificate and has never told the mesh what key it serves with; it joins again to report one" { + t.Fatalf("no serving key: %v", err) + } + laptop.ServingKey = "serving-key" + got := rendered(t, in, "laptop", Foreseeing) + if got.Rendering.Certificate != ForeseenCertificate || got.Rendering.Authority != ForeseenCertificate || len(got.Foreseen) != 1 { + t.Errorf("no certificate in the Input: %q %q foreseen %v", got.Rendering.Certificate, got.Rendering.Authority, got.Foreseen) + } + laptop.Certificate, in.Authority = "the-certificate", "the-authority" + got = rendered(t, in, "laptop", Foreseeing) + if got.Rendering.Certificate != "the-certificate" || got.Rendering.Authority != "the-authority" || len(got.Foreseen) != 0 { + t.Errorf("a certificate given: %q %q foreseen %v", got.Rendering.Certificate, got.Rendering.Authority, got.Foreseen) + } + laptop.Place.Address = "" // placed nowhere: no name inside the mesh + if _, err := Render(in, "laptop", Foreseeing); err == nil || err.Error() != "laptop wants a certificate and is not on the private network, so it has no name inside the mesh to be certified for" { + t.Fatalf("off the network: %v", err) + } +} + +func aValidKey(t *testing.T, name string) string { return keyOf(t, name) } + +// 1364/AC5 and AC6: a core credential the node needs and the Input holds no box for is composed with a stand-in and returned as one +// to keep; a kept box sealed to the key held now is carried; a node-engine below the vault-box contract is sent none (and nothing is +// asked of it); a machine with no sealing key has its core module left out, said. +func TestACoreBoxTheInputHoldsNoneOfIsReturnedToKeepAndNothingIsWritten(t *testing.T) { + mk := func() Input { + in := aMesh(t) + in.Catalogue["nats"] = catalogue.Manifest{Module: "nats", Version: "1", + OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mesh/nats/broker", Taken: catalogue.TakenAtStart}}} + laptop := nodeOf(t, &in, "laptop") + laptop.Assigned = append(laptop.Assigned, "nats") + laptop.SealingKey = aValidKey(t, "laptop") + laptop.Stated, laptop.Contract = true, VaultBoxContract + in.BusMinted = []string{"laptop.nats"} + return in + } + in := mk() + got := rendered(t, in, "laptop", Foreseeing) + box := got.Rendering.VaultSealed["nats"]["broker"] + if string(box) != string(catalogue.ForeseenVaultBox) || len(got.CoreBoxesWanted) != 1 || + got.CoreBoxesWanted[0] != (CoreBoxWanted{Node: "laptop", Module: "nats", Name: "broker"}) { + t.Errorf("no box held: %s wanted %v", box, got.CoreBoxesWanted) + } + kept := json.RawMessage(`{"v":1,"eph":"e","iv":"i","tag":"t","ct":"c"}`) + in.KeptCoreBoxes = []CoreBox{{Node: "laptop", Module: "nats", Name: "broker", Generation: 3, SealedTo: nodeOf(t, &in, "laptop").SealingKey, Box: kept}} + got = rendered(t, in, "laptop", Foreseeing) + if string(got.Rendering.VaultSealed["nats"]["broker"]) != string(kept) || len(got.CoreBoxesWanted) != 0 { + t.Errorf("a kept box: %s wanted %v", got.Rendering.VaultSealed["nats"]["broker"], got.CoreBoxesWanted) + } + in.KeptCoreBoxes[0].SealedTo = "an-old-key" + if got = rendered(t, in, "laptop", Foreseeing); len(got.CoreBoxesWanted) != 1 { + t.Errorf("a box sealed to an old key is kept: %v", got.CoreBoxesWanted) + } + // Below the vault-box contract: none, and the controller's own seal as before. + nodeOf(t, &in, "laptop").Contract = VaultBoxContract - 1 + if got = rendered(t, in, "laptop", Foreseeing); got.Rendering.VaultSealed != nil || len(got.CoreBoxesWanted) != 0 { + t.Errorf("an engine below the contract is sent boxes: %v %v", got.Rendering.VaultSealed, got.CoreBoxesWanted) + } + // No sealing key: the module is left out, said. + nodeOf(t, &in, "laptop").Contract, nodeOf(t, &in, "laptop").SealingKey = VaultBoxContract, "" + got = rendered(t, in, "laptop", Reading) + if why := got.Rendering.NotPlaced["nats"]; !strings.Contains(why, "laptop has no sealing key, so the vault can seal nats's credentials to nothing there") { + t.Errorf("not placed: %v", got.Rendering.NotPlaced) + } + unchanged(t, mk, func(in Input) { rendered(t, in, "laptop", Foreseeing) }) +} + +// 1364/AC1: the bus's user list is carried for the machine that runs the bus and for no other; without it a stand-in is composed and +// named; the users left out for want of a credential are said; each machine's membership is carried. (The list is composed by the +// controller from the bus records, which stay the controller's: see the commit message.) +func TestTheBusUserListIsCarriedForTheMachineThatRunsTheBus(t *testing.T) { + in := aMesh(t) + in.Catalogue["bus"] = catalogue.Manifest{Module: "bus", Version: "1", BusUsers: "/etc/bus/accounts.conf", + Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}} + anchor := nodeOf(t, &in, "anchor") + anchor.Assigned = append(anchor.Assigned, "bus") + in.BusUsers = "accounts { anchor.bus: hash }" + in.BusMissing = []string{"laptop.files", "laptop.web"} + in.BusMemberships = map[string]string{"laptop": "sealed-membership"} + got := rendered(t, in, "anchor", Foreseeing) + if got.Rendering.BusUsers != in.BusUsers || len(got.Foreseen) != 0 { + t.Errorf("the list is %q foreseen %v", got.Rendering.BusUsers, got.Foreseen) + } + if len(got.Said) != 1 || got.Said[0] != "the bus's user list leaves out 2 user(s) the mesh has minted no credential for: laptop.files, laptop.web. Each is a user that cannot connect until one is issued" { + t.Errorf("said %v", got.Said) + } + if other := rendered(t, in, "laptop", Foreseeing); other.Rendering.BusUsers != "" || other.Rendering.BusMembership != "sealed-membership" { + t.Errorf("laptop: list %q membership %q", other.Rendering.BusUsers, other.Rendering.BusMembership) + } + in.BusUsers = "" + got = rendered(t, in, "anchor", Foreseeing) + if got.Rendering.BusUsers != ForeseenBusUsers || len(got.Foreseen) != 1 { + t.Errorf("no list in the Input: %q foreseen %v", got.Rendering.BusUsers, got.Foreseen) + } +} + +// 1364/AC7: nothing the composition says carries a secret value the Input holds. +func TestNoSecretValueOfTheInputIsInWhatTheCompositionSays(t *testing.T) { + in := aGrantMesh(t) + in.Issued[0].ForProvider = secretValue + laptop := nodeOf(t, &in, "laptop") + in.Catalogue["assistant"] = anAssistant() + laptop.Assigned = append(laptop.Assigned, "assistant") + laptop.SealingKey = keyOf(t, "laptop") + in.OwnSecrets = []OwnSecretRow{{Node: "laptop", Module: "assistant", Name: "password", Origin: "made", SealedTo: laptop.SealingKey, Sealed: secretValue}} + in.Credentials = []Credential{{Provision: "s3-bucket", Consumer: "laptop", ConsumerModule: "files", Provider: "anchor", State: CredentialHeld, Sealed: secretValue}} + for _, node := range []string{"anchor", "laptop"} { + got := rendered(t, in, node, Foreseeing) + for _, said := range append(append([]string{}, got.Foreseen...), got.Said...) { + if strings.Contains(said, secretValue) { + t.Errorf("%s: %q carries a secret value", node, said) + } + } + } +}