diff --git a/modules/mesh-vault/cmd/mesh-vault/licencebindings.go b/modules/mesh-vault/cmd/mesh-vault/licencebindings.go index d1d09422a..bb85f20a5 100644 --- a/modules/mesh-vault/cmd/mesh-vault/licencebindings.go +++ b/modules/mesh-vault/cmd/mesh-vault/licencebindings.go @@ -12,6 +12,7 @@ import ( "errors" "fmt" "sort" + "strings" "sync" "time" @@ -229,3 +230,43 @@ func watchLicenceBindings(lb LicenceBindings) bool { } return true } + +// licenceRefusal answers whether name is a licence's secret and, if it is, why the vault does not hand it to caller, +// empty when it does (§16, §17, A1, B3, B8, B25): a grant to any caller (B3), an access token to a machine on which a +// consumer's binding names that licence's key. The rule that a value goes only to a module under a bus account of +// its own does not apply to a licence's secrets (§17). +func (v *Vault) licenceRefusal(caller, name string) (bool, string) { + rest, ok := strings.CutPrefix(name, "licence.") + if !ok { + return false, "" + } + key, part := rest, "" + for _, p := range []string{LicenceGrant, LicenceAccess} { + if k, cut := strings.CutSuffix(rest, "."+p); cut { + key, part = k, p + } + } + if part == "" || !licenceKey.MatchString(key) { + return false, "" + } + if part == LicenceGrant { + return true, "" + } + machine, _, _ := strings.Cut(caller, ".") + if v.LicenceBindings == nil { + return true, "the vault keeps no licence bindings, so no binding names the licence " + key + } + consumers, err := v.LicenceBindings.Keys() + if err != nil { + return true, "the licence bindings cannot be read: " + err.Error() + } + for _, consumer := range consumers { + if c, _, _ := strings.Cut(consumer, "."); c != machine { + continue + } + if b, err := v.LicenceBindings.Get(consumer); err == nil && b != nil && b.Key == key { + return true, "" + } + } + return true, fmt.Sprintf("no consumer on %s is bound to the licence %s, and its token goes only to a machine a binding names", machine, key) +} diff --git a/modules/mesh-vault/cmd/mesh-vault/licences_test.go b/modules/mesh-vault/cmd/mesh-vault/licences_test.go index dafc5dc0c..373ad630a 100644 --- a/modules/mesh-vault/cmd/mesh-vault/licences_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/licences_test.go @@ -56,3 +56,45 @@ func TestLicencePutKeepsTheNewestGenerationAndRefusesAnOlder(t *testing.T) { t.Error("licence-put took an unproven caller") } } + +// design 39 bus-and-switch §17/AC1, §16, B3, B8, B25: a licence's access token is refused to a machine no binding names +// and for a licence its binding does not name, and given after a binding.changed naming it, to any consumer of that +// machine; the grant goes to the manager's tool runner, with no machine guard. +func TestATokenIsGivenOnlyToAMachineABindingNames(t *testing.T) { + v := newVault(t, nil) + lb, _ := memLicenceBindings() + v.LicenceBindings = &lb + const manager = "novox.node-tools" + for _, key := range []string{"jane_at_example.org", "api"} { + for _, part := range []string{LicenceGrant, LicenceAccess} { + if _, err := putLicence(t, v, manager, key, part, 1, key+"-"+part); err != nil { + t.Fatal(err) + } + } + } + access := "licence.jane_at_example.org.access" + if _, _, err := asks(v, "g14.node-tools", access); err == nil || !strings.Contains(err.Error(), "no consumer on g14") { + t.Fatalf("a machine no binding names was handed the token: %v", err) + } + if err := lb.Heard([]byte(`{"consumer":"g14","licence":"api","key":"api","generation":4}`)); err != nil { + t.Fatal(err) + } + if _, _, err := asks(v, "g14.node-tools", access); err == nil { + t.Fatal("a licence its binding does not name was handed to g14") + } + if got, _, err := asks(v, "g14.node-tools", "licence.api.access"); err != nil || got != "api-access" { + t.Fatalf("g14's own licence: %q %v", got, err) + } + if err := lb.Heard([]byte(`{"consumer":"g14.worker-1","licence":"jane@example.org","key":"jane_at_example.org","generation":2}`)); err != nil { + t.Fatal(err) + } + if got, _, err := asks(v, "g14.node-tools", access); err != nil || got != "jane_at_example.org-access" { + t.Fatalf("after a binding.changed naming it: %q %v", got, err) + } + if _, _, err := asks(v, "ace.node-tools", access); err == nil { + t.Fatal("another machine was handed g14's token") + } + if got, _, err := asks(v, manager, "licence.jane_at_example.org.grant"); err != nil || got != "jane_at_example.org-grant" { + t.Fatalf("the manager's grant: %q %v", got, err) + } +} diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index 2d2a1126b..129a44acc 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -58,7 +58,8 @@ func build() *Env { // The assignments as the vault's watch keeps them (watchAssignments), read over the bus until it has caught up. assigned = &WatchedAssignments{Fallback: readPlanRaw} watchedPlan := func(key string) ([]byte, error) { raw, err := assigned.Read(key); return asAssignment(key, raw, err) } - env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store}, + licenceBindings := busLicenceBindings() + env.Vault = &Vault{Store: store, Own: own, LicenceBindings: &licenceBindings, Askers: Declared{Read: watchedPlan, Confirm: readAssignment, Store: store}, Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask, Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, WantRead: readWant, HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Assigner: seatAssignments, Signers: busSigners{}, diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 85c0c6ccf..aac01cdf1 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -28,6 +28,9 @@ const ChangedEvent = "secret-changed" // Vault holds the secrets. type Vault struct { Store *Store + // LicenceBindings is the vault's own state of the licence bindings (licencebindings.go): which machine a licence's + // access token goes to. Nil hands it to no machine. + LicenceBindings *LicenceBindings // Own is the vault's own key; the private half never leaves its state directory. Own seal.KeyPair // Askers decides who may ask `get` for what. Nil allows nobody. @@ -386,8 +389,16 @@ func (v *Vault) Get(caller, name, publicKey string) (Answer, error) { _ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: why}) return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, why) } + isLicence, licenceWhy := v.licenceRefusal(caller, name) if isLogin { // Judged above: the node's own engine, sealed to its own key. + } else if isLicence { + // A licence's secret, by the licence rule (design 39 bus-and-switch §16, §17). + if licenceWhy != "" { + v.log("refused %s to %s: %s", name, caller, licenceWhy) + _ = v.recordHanding(Handing{To: caller, Name: name, Outcome: "refused", Why: licenceWhy}) + return Answer{}, fmt.Errorf("%s may not ask for %s: %s", caller, name, licenceWhy) + } } else if ok, why := askers.May(caller, name); !ok { if reason, notYet := strings.CutPrefix(why, notYetPrefix); notYet { // Not a refusal (issue 928): the client waits and asks again, and nothing is recorded against it.