Merge pull request 'Vault: adopt the hashes of bus accounts it holds no value for, handed sealed by the bus node (issue 1945, ADR 0550 clause 4)' (#494) from feat/1945-user-list-switch into main

This commit was merged in pull request #494.
This commit is contained in:
2026-10-26 12:21:47 +00:00
11 changed files with 511 additions and 15 deletions
+11 -4
View File
@@ -14,8 +14,8 @@ package main
// bus refused is kept as owed and tried again at the next minute pass, never lost silently.
//
// The hashes of accounts the vault holds no value for (enrolment users, persons, the controller's own) are adopted once,
// before take-over, from the list the bus loaded (clause 9's adoption): such a key is marked adopted and is replaced by
// the vault's own hash the moment the vault holds the account's value, never the other way round.
// before take-over, from the list the bus loaded (clause 9's adoption, adopt.go): such a key is marked adopted and is
// replaced by the vault's own hash the moment the vault holds the account's value, never the other way round.
import (
"encoding/json"
@@ -48,14 +48,19 @@ type AccountHash struct {
Generation int `json:"generation,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
At time.Time `json:"at"`
// Adopted marks a hash taken from the list the bus loaded, for an account the vault holds no value for.
Adopted bool `json:"adopted,omitempty"`
// Adopted marks a hash taken from the list the bus loaded, for an account the vault holds no value for; From is
// that list's digest and Node the node that loaded it and handed it (adopt.go).
Adopted bool `json:"adopted,omitempty"`
From string `json:"from,omitempty"`
Node string `json:"node,omitempty"`
}
// AccountStore is the vault's state of bus-account hashes: read and written by the vault's instances alone.
type AccountStore interface {
Get(user string) (*AccountHash, error)
Put(a AccountHash) error
// Users is every user with a key.
Users() ([]string, error)
}
// busAccounts is AccountStore over the vault's own state through its runtime.
@@ -73,6 +78,8 @@ func (busAccounts) Get(user string) (*AccountHash, error) {
return &a, nil
}
func (busAccounts) Users() ([]string, error) { return stdio.State(BusAccountsState).Keys() }
func (busAccounts) Put(a AccountHash) error {
_, err := stdio.State(BusAccountsState).Put(a.User, a)
return err
+276
View File
@@ -0,0 +1,276 @@
package main
// The vault adopts the hashes of the bus accounts it holds no value for (novox/hq ADR 0547 clause 9, issue 1945).
//
// The controller's own account, the node accounts and the enrolled devices were never handed to the vault, so it cannot
// hash them, and the grants part's list would leave them out. The one part that has their hashes is the node-engine of
// the node holding mesh-broker, in the list the bus loaded: it hands every loaded user's hash in the bus module's have,
// sealed to this vault's key (mesh-sdk delivery.AccountsHanded), which it learns from the vault's answer to node-key.
// The vault reads that have (its manifest `reads` node-engine.have), as a watch of the bus module's have and again by
// hand with the tool `account_adopt`, and opens it with its own key.
//
// Of the users handed it takes only those it holds no value for: a user whose covered account it holds, or whose key in
// bus-accounts is its own hash, is never touched (the hash it made from the value is the one that holds). Each other
// user's hash is written to bus-accounts.<user> marked adopted, with the list it came from, saying account-hashed; an
// adopted key is written again only when the loaded list carries another hash for the user. A handing from any node but
// the one the assignments name as holding the bus module is refused: a node-engine writes only its own node's haves, so
// that node is the one that loaded the list.
import (
"errors"
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// HaveRead is the node-engines' have bucket as the vault reads it (its manifest's `reads`).
const HaveRead = "node-engine.have"
// BusModule is the module holding mesh-broker, whose have on its node carries the handing.
const BusModule = "nats"
// HaveReader reads one key of the have bucket: nil when there is none.
type HaveReader func(key string) ([]byte, error)
// Adoption is what one adoption did: never a hash.
type Adoption struct {
Node string `json:"node"`
// From is the digest of the loaded list the handing is of; Handed how many users it holds.
From string `json:"from,omitempty"`
Handed int `json:"handed"`
// Adopted are the users whose hash was written now; Same those adopted before with the same hash; Own those the
// vault holds a value for, never adopted.
Adopted []string `json:"adopted"`
Same int `json:"same"`
Own int `json:"own"`
Refused map[string]string `json:"refused,omitempty"`
At time.Time `json:"at"`
By string `json:"by"`
}
// adoptionKept is what the vault keeps of its last adoption, for its tools and to skip a have that says the same.
type adoptionKept struct {
last *Adoption
handed string // the From and To of the handing last taken in a complete pass
failed string // why the last look found nothing to adopt, or ""
}
// busNode is the node the assignments name as holding the bus module: exactly one.
func (v *Vault) busNode() (string, error) {
if v.Assignments.Keys == nil {
return "", errors.New("the vault cannot read the assignments, so it cannot tell which node holds the bus")
}
keys, err := v.Assignments.Keys()
if err != nil {
return "", fmt.Errorf("the assignments' keys could not be read: %w", err)
}
var nodes []string
for _, k := range keys {
if node, module, ok := strings.Cut(k, "."); ok && module == BusModule {
nodes = append(nodes, node)
}
}
sort.Strings(nodes)
switch len(nodes) {
case 1:
return nodes[0], nil
case 0:
return "", fmt.Errorf("no node is assigned the bus module %s", BusModule)
}
return "", fmt.Errorf("%d nodes are assigned the bus module %s (%s): the vault adopts from one alone", len(nodes),
BusModule, strings.Join(nodes, ", "))
}
// AdoptNow is the tool account_adopt: the bus node's have read now and adopted, though it says the same as before.
func (v *Vault) AdoptNow(caller string) (Adoption, error) {
if v.Haves == nil {
return Adoption{}, errors.New("the vault cannot read the have bucket (" + HaveRead + ")")
}
node, err := v.busNode()
if err != nil {
return Adoption{}, err
}
raw, err := v.Haves(node + "." + BusModule)
if err != nil {
return Adoption{}, fmt.Errorf("%s %s.%s could not be read: %w", HaveRead, node, BusModule, err)
}
if raw == nil {
return Adoption{}, fmt.Errorf("%s holds no have of %s on %s", HaveRead, BusModule, node)
}
return v.AdoptFrom(node, raw, someoneOr(caller, "a tool"), true)
}
// AdoptFrom adopts from the have of the bus module on node: unless force, nothing when it hands what the last complete
// adoption took.
func (v *Vault) AdoptFrom(node string, raw []byte, by string, force bool) (Adoption, error) {
a, err := v.adopt(node, raw, by, force)
v.mu.Lock()
switch {
case err != nil:
v.adoption.failed = err.Error()
case a.At.IsZero():
default:
v.adoption.failed = ""
v.adoption.last = &a
}
v.mu.Unlock()
if err == nil && len(a.Adopted) > 0 {
v.log("adopted the hashes of %d bus accounts it holds no value for from %s's loaded list %s (%s), asked by %s",
len(a.Adopted), node, a.From, strings.Join(a.Adopted, ", "), by)
}
if len(a.Refused) > 0 {
v.log("ADOPTION REFUSED %d of the users %s handed: %v", len(a.Refused), node, a.Refused)
}
return a, err
}
func (v *Vault) adopt(node string, raw []byte, by string, force bool) (Adoption, error) {
if v.Accounts == nil {
return Adoption{}, errors.New("the vault has no state of bus accounts to write to")
}
bus, err := v.busNode()
if err != nil {
return Adoption{}, err
}
if node != bus {
return Adoption{}, fmt.Errorf("%s does not hold the bus (the assignments name %s): its handing is not adopted",
node, bus)
}
have, err := delivery.DecodeHave(raw)
if err != nil {
return Adoption{}, fmt.Errorf("the have of %s on %s is not a have: %w", BusModule, node, err)
}
if have.UserList == nil || have.UserList.Handed == nil {
return Adoption{}, fmt.Errorf("the have of %s on %s hands no hashes yet (its node-engine hands them once it "+
"heard the vault's key in its answer to node-key, and writes the list)", BusModule, node)
}
h := *have.UserList.Handed
handed := h.From + " " + h.To
v.mu.Lock()
seen := v.adoption.handed == handed
v.mu.Unlock()
if seen && !force {
return Adoption{}, nil
}
accounts, err := delivery.OpenHanded(h, v.Own.PublicKey, v.Own.PrivateKey)
if err != nil {
return Adoption{}, err
}
v.hashMu.Lock()
defer v.hashMu.Unlock()
held, err := v.heldUsers()
if err != nil {
return Adoption{}, err
}
a := Adoption{Node: node, From: h.From, Handed: len(accounts), Adopted: []string{}, At: v.Now().UTC(), By: by}
refuse := func(user, why string) {
if a.Refused == nil {
a.Refused = map[string]string{}
}
a.Refused[user] = why
}
for _, acc := range accounts {
if !delivery.IsBcrypt(acc.Hash) || !namePart.MatchString(acc.User) {
refuse(acc.User, "not a bus user with a bcrypt hash")
continue
}
if held[acc.User] {
a.Own++
continue
}
was, err := v.Accounts.Get(acc.User)
switch {
case err != nil:
refuse(acc.User, "its key could not be read: "+err.Error())
continue
case was != nil && !was.Adopted:
a.Own++
continue
case was != nil && was.Hash == acc.Hash:
a.Same++
continue
}
if err := v.Accounts.Put(AccountHash{User: acc.User, Hash: acc.Hash, At: a.At, Adopted: true, From: h.From,
Node: node}); err != nil {
refuse(acc.User, "its key could not be written: "+err.Error())
continue
}
a.Adopted = append(a.Adopted, acc.User)
if v.Emit != nil {
if err := v.Emit(AccountHashedEvent, map[string]any{"user": acc.User, "adopted": true, "from": h.From,
"node": node, "at": a.At.Format(time.RFC3339Nano)}); err != nil {
v.log("the adopted hash of %s is written, but %s could not be said: %v", acc.User, AccountHashedEvent, err)
}
}
}
// A complete pass, refusals and all: the same handing is not opened again at the next change of the have (a
// refused user stays refused until the list changes, and the last adoption names it).
v.mu.Lock()
v.adoption.handed = handed
v.mu.Unlock()
return a, nil
}
// heldUsers is the bus user of every covered account the vault holds a value for.
func (v *Vault) heldUsers() (map[string]bool, error) {
records, err := v.Store.List()
if err != nil {
return nil, err
}
out := map[string]bool{}
for _, r := range records {
if !Covered(r.Name) {
continue
}
value, err := openAtRest(r, v)
if err != nil {
return nil, fmt.Errorf("the vault could not open its copy of %s, so it cannot tell whose value it holds: %w",
r.Name, err)
}
if user, _, err := accountOf(r.Name, value); err == nil {
out[user] = true
}
}
return out, nil
}
// adoptedRows is every adopted key of bus-accounts, for secret_own: never a hash.
func (v *Vault) adoptedRows() (rows []map[string]any, err error) {
if v.Accounts == nil {
return nil, nil
}
users, err := v.Accounts.Users()
if err != nil {
return nil, err
}
sort.Strings(users)
for _, u := range users {
a, err := v.Accounts.Get(u)
if err != nil || a == nil || !a.Adopted {
continue
}
rows = append(rows, map[string]any{"user": a.User, "adoptedAt": a.At, "from": a.From, "node": a.Node})
if len(rows) >= ownRowsMax {
break
}
}
return rows, nil
}
// adoptionSaid is the last adoption and why the last look adopted nothing, for the tools.
func (v *Vault) adoptionSaid() map[string]any {
v.mu.Lock()
defer v.mu.Unlock()
out := map[string]any{"last": v.adoption.last}
if v.adoption.last != nil && len(v.adoption.last.Refused) > 0 {
out["refused"] = v.adoption.last.Refused
}
if v.adoption.failed != "" {
out["why"] = v.adoption.failed
}
return out
}
@@ -48,6 +48,16 @@ func (f *fakeAccounts) Put(a AccountHash) error {
return nil
}
func (f *fakeAccounts) Users() ([]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []string
for u := range f.keys {
out = append(out, u)
}
return out, nil
}
// hashOf says whether the key of user is the hash of password at generation, of the vault's own value.
func (f *fakeAccounts) hashOf(user, password string, generation int) bool {
f.mu.Lock()
+46 -1
View File
@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"os"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
@@ -50,10 +51,50 @@ func build() *Env {
// Where it sees a consumer bind a provider (pair.go): the same mirror, every key.
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: readAssignment, Store: store},
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
Now: time.Now, Log: say, Accounts: busAccounts{}}
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave}
return env
}
// readHave is one key of the node-engines' have bucket, read through the runtime under the vault's own account (its
// manifest `reads` it), or nil when there is none.
func readHave(key string) ([]byte, error) {
e, err := stdio.State(HaveRead).Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
}
// watchHandings adopts from the bus module's have whenever it changes (adopt.go): a watch, never a poll. It answers
// whether the watch began; one that did not is begun again at the next minute pass.
func watchHandings(env *Env) bool {
if env.Vault == nil {
return true
}
// The have changes at every look of the bus module's health: why nothing was adopted is said once, and again when
// it changes.
var said string
err := stdio.State(HaveRead).Watch("*."+BusModule, func(c stdio.StateChange) error {
if c.Op == "delete" || len(c.Value) == 0 {
return nil
}
node := strings.TrimSuffix(c.Key, "."+BusModule)
_, err := env.Vault.AdoptFrom(node, c.Value, "the watch of "+HaveRead, false)
if line := fmt.Sprint(err); err != nil && line != said {
say("adopted nothing from %s: %v", c.Key, err)
said = line
} else if err == nil {
said = ""
}
return nil
})
if err != nil {
say("the bus module's have is not watched, so no hash is adopted; begun again at the next pass: %v", err)
return false
}
return true
}
// adoptOperatorKey takes the operator key from the export the mesh writes to the vault's root (what
// `operator key set` recorded; the controller keeps only a public half there), and seals again any
// operator copy not yet sealed to the current key. Run every minute, so a failure partway is finished. It
@@ -107,7 +148,11 @@ func main() {
written, failed := env.Vault.HashHeld()
say("hashed %d bus accounts at start; %d could not be, tried again every minute: %v", len(written), len(failed), failed)
}
watched := watchHandings(env)
for {
if !watched {
watched = watchHandings(env)
}
env.pass()
if env.Vault != nil {
env.Vault.HashOwed()
+33 -4
View File
@@ -235,7 +235,8 @@ func definitions(env *Env) []toolDef {
Description: "State this machine's sealing key to the vault, in its own name (ADR 0372, issue 786): from the " +
"node-engine of the machine alone, at its start and whenever the key changes; the machine is the caller's, " +
"as the bus proved it. A first or changed key is recorded and `secret-changed` is raised for the machine's " +
"core secrets, so the controller asks again; the same key again changes nothing.",
"core secrets, so the controller asks again; the same key again changes nothing. Answers the vault's own " +
"public key (`vault_key`, SPKI PEM), which the bus node seals the loaded list's hashes to (ADR 0547 clause 9).",
Input: map[string]any{"type": "object", "properties": map[string]any{
"key": str("this machine's sealing key, a raw X25519 public key in base64, as move-in's sealing_key carries it")}},
run: func(caller string, a map[string]any) (any, error) {
@@ -246,7 +247,9 @@ func definitions(env *Env) []toolDef {
if err := v.StateNodeKey(caller, arg(a, "key")); err != nil {
return nil, err
}
return map[string]any{"recorded": true}, nil
// And the vault's own public key, which the bus node seals the loaded list's hashes to (ADR 0547
// clause 9, adopt.go): a public half, never the private one.
return map[string]any{"recorded": true, "vault_key": v.Own.PublicKey}, nil
}},
{Name: Seat + ".move-asks",
Description: "Copy rows of the controller's record of asks at a desk into the vault's, as they are and once " +
@@ -347,14 +350,40 @@ func definitions(env *Env) []toolDef {
"(sha256:<hex>), when it was made and last changed, by whom, where the value came from, and whether it is a " +
"covered bus account; for a bus account, its user and whether the hash of this generation is written in " +
"`bus-accounts` (hashed, hashedAt, or why not). Narrow by node, module or name prefix. At most 1000 rows; " +
"says when cut. Never a value or a hash.",
"says when cut. Unnarrowed by module, also `adopted`: every account whose hash the vault adopted from the " +
"bus's loaded list because it holds no value for it (user, adoptedAt, the list's digest, the node), and " +
"`adoption`: the last adoption and why the last look adopted nothing. Never a value or a hash.",
Input: map[string]any{"node": str("only this machine's secrets"), "module": str("only this module's secrets"),
"name": str("only secrets whose name (the last part) starts with this, e.g. broker")},
run: func(_ string, a map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
return ownAnswer(env.Vault, arg(a, "node"), arg(a, "module"), arg(a, "name"))
out, err := ownAnswer(env.Vault, arg(a, "node"), arg(a, "module"), arg(a, "name"))
if err != nil || arg(a, "module") != "" {
return out, err
}
adopted, err := env.Vault.adoptedRows()
if err != nil {
out["adoptedWhy"] = "bus-accounts could not be read: " + err.Error()
}
if adopted == nil {
adopted = []map[string]any{}
}
out["adopted"], out["adoption"] = adopted, env.Vault.adoptionSaid()
return out, nil
}},
{Name: "account_adopt",
Description: "Adopt now the hashes of the bus accounts the vault holds no value for (ADR 0547 clause 9): read " +
"the have of the bus module on the node holding it, open the loaded list's hashes its node-engine handed " +
"sealed to the vault's key, and write each user's into `bus-accounts` marked adopted, saying " +
"`account-hashed`. Never a user whose value the vault holds or whose key is its own hash. As the watch of " +
"that have does, though the handing is the one adopted before. Answers the users adopted and counted, never a hash.",
run: func(caller string, _ map[string]any) (any, error) {
if env.Vault == nil {
return nil, errors.New("the vault is not ready")
}
return env.Vault.AdoptNow(caller)
}},
{Name: "account_hash",
Description: "Hash one covered bus account again now (ADR 0547 clause 9): the vault writes the bcrypt hash of " +
@@ -4,6 +4,7 @@ package main
// secret_verify — none returns a value. And the seat's verb `get`, reached the way the runtime calls it.
import (
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
@@ -11,6 +12,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seal"
)
@@ -218,4 +220,113 @@ func TestSecretOwnListsFingerprintsOfOwnSecretsOnly(t *testing.T) {
if _, err := hash.Run(map[string]any{"user": "ace.mesh-build-go"}); err == nil {
t.Error("account_hash hashed a value that is no account")
}
// ADR 0547 clause 9: the hashes of the accounts the vault holds no value for are adopted from the bus node's
// handing, sealed to the vault's key (which node-key answers), never over a user whose value it holds, again only
// when the loaded list carries another hash, and only from the node the assignments name as holding the bus.
defs := definitions(&Env{Vault: v.Vault})
answer, err := toolNamed(t, defs, Seat+".node-key").runAs("node.novox",
map[string]any{"key": base64.StdEncoding.EncodeToString(make([]byte, 32))})
if err != nil || answer.(map[string]any)["vault_key"] != v.Own.PublicKey {
t.Fatalf("node-key answered %v, %v: no vault key", answer, err)
}
h := func(c string) string { return "$2a$11$" + strings.Repeat(c, 53) }
handing := map[string]string{"controller": h("c"), "node.ace": h("n"), "g14.dunst": h("d")}
sealedTo := v.Own.PublicKey
have := func() []byte {
var accounts []delivery.HandedAccount
for _, u := range []string{"controller", "node.ace", "g14.dunst", "bad.user"} {
if hash, ok := handing[u]; ok {
accounts = append(accounts, delivery.HandedAccount{User: u, Hash: hash})
}
}
// The list's digest, as the node says it: another list (another hash or user) is another digest.
list, _ := json.Marshal(accounts)
handed, err := delivery.HandAccounts(accounts, sealedTo, Fingerprint(string(list)), time.Now())
if err != nil {
t.Fatal(err)
}
raw, _ := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK,
UserList: &delivery.UserListWritten{Source: "k", Users: 3, Handed: &handed}})
return raw
}
v.Assignments = Mirror{Keys: func() ([]string, error) { return []string{"novox.nats", "novox.mesh-vault", "ace.dunst"}, nil }}
v.Haves = func(key string) ([]byte, error) {
if key != "novox.nats" {
return nil, nil
}
return have(), nil
}
dunst := acc.keys["g14.dunst"]
adopt := toolNamed(t, defs, "account_adopt")
for _, step := range []struct {
name string
change func()
adopted []string
same, own int
wantErr, said string
}{
{name: "the users it holds no value for are adopted", adopted: []string{"controller", "node.ace"}, own: 1},
{name: "again: the same hashes are kept", adopted: []string{}, same: 2, own: 1},
{name: "another hash in the loaded list is adopted again", change: func() { handing["node.ace"] = h("m") },
adopted: []string{"node.ace"}, same: 1, own: 1},
{name: "a handing sealed to another key is refused", change: func() {
other, _ := seal.GenerateKeyPair()
sealedTo = other.PublicKey
}, wantErr: "not to this one"},
} {
if step.change != nil {
step.change()
}
before := len(v.bus.events)
got, err := adopt.Run(map[string]any{})
if step.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), step.wantErr) {
t.Fatalf("%s: %v %+v", step.name, err, got)
}
continue
}
a, _ := got.(Adoption)
if err != nil || strings.Join(a.Adopted, ",") != strings.Join(step.adopted, ",") || a.Same != step.same ||
a.Own != step.own || a.Handed != 3 || eventsNamed(v, before, AccountHashedEvent) != len(step.adopted) {
t.Fatalf("%s: %+v, %v", step.name, a, err)
}
raw, _ := json.Marshal(got)
for _, hash := range handing {
if strings.Contains(string(raw), hash) {
t.Fatalf("%s: account_adopt said a hash: %s", step.name, raw)
}
}
}
if k := acc.keys["controller"]; !k.Adopted || k.Hash != h("c") || k.Node != "novox" || acc.keys["node.ace"].Hash != h("m") {
t.Errorf("adopted keys %+v %+v", k, acc.keys["node.ace"])
}
if acc.keys["g14.dunst"] != dunst {
t.Errorf("a user whose value the vault holds was adopted over: %+v", acc.keys["g14.dunst"])
}
// A refused user is named in the last adoption, and the same handing is not opened again at the next change.
sealedTo, handing["bad.user"] = v.Own.PublicKey, "not-bcrypt"
if a, err := v.AdoptFrom("novox", have(), "the watch", false); err != nil || a.Refused["bad.user"] == "" || a.Handed != 4 {
t.Fatalf("a refused user: %+v, %v", a, err)
}
if a, err := v.AdoptFrom("novox", have(), "the watch", false); err != nil || !a.At.IsZero() {
t.Fatalf("the same handing was opened again after a refusal: %+v, %v", a, err)
}
if said := v.adoptionSaid(); said["refused"] == nil {
t.Errorf("the refusal is not said: %v", said)
}
if _, err := v.AdoptFrom("ace", have(), "the test", true); err == nil || !strings.Contains(err.Error(), "does not hold the bus") {
t.Errorf("a handing from a node not holding the bus: %v", err)
}
listed, err := own.Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
rows := listed.(map[string]any)["adopted"].([]map[string]any)
if len(rows) != 2 || rows[0]["user"] != "controller" || rows[1]["user"] != "node.ace" || rows[0]["node"] != "novox" {
t.Errorf("secret_own's adopted rows %v", rows)
}
if narrowed, _ := own.Run(map[string]any{"module": "dunst"}); narrowed.(map[string]any)["adopted"] != nil {
t.Errorf("secret_own narrowed to a module lists adopted accounts: %v", narrowed)
}
}
@@ -46,11 +46,16 @@ type Vault struct {
Accounts AccountStore
// HashCost is the bcrypt cost of those hashes: zero is the controller's, bcrypt's default.
HashCost int
// Haves reads the node-engines' have bucket, where the bus node hands the loaded list's hashes (adopt.go): nil
// adopts nothing.
Haves HaveReader
mu sync.Mutex
// hashMu keeps one hashing at a time, and hashes what is owed (accounts.go).
hashMu sync.Mutex
hashes hashing
// adoption is the last adoption (adopt.go), under mu.
adoption adoptionKept
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
callsMu sync.Mutex
pending map[string]*callKey
@@ -131,8 +131,9 @@ func filesUnder(t *testing.T, dir string) map[string]string {
// The manifest declares one bucket, and no secret is in it: the vault's only place for a secret is its own store on its
// own disk (ADR 0349 §7, ADR 0201 §13). The one bucket is bus-accounts, the bcrypt hashes of the bus accounts it holds
// (ADR 0547 clause 9), opened in accounts.go alone. It reads one bucket of another's, and only reads it: the
// controller's assignments, for what each node's plan names (askers.go, design 57 V4).
// (ADR 0547 clause 9), opened in accounts.go alone. It reads two buckets of others', and only reads them: the
// controller's assignments, for what each node's plan names (askers.go, design 57 V4), and the node-engines' haves, where
// the bus node hands the loaded list's hashes sealed to the vault (adopt.go, ADR 0547 clause 9), in main.go alone.
func TestNoBucketHoldsASecret(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
@@ -146,8 +147,8 @@ func TestNoBucketHoldsASecret(t *testing.T) {
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the one "+
"bucket is the accounts' hashes (ADR 0547 clause 9)", s)
}
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments"]` {
t.Errorf("the manifest reads %s: the vault reads the controller's assignments alone", r)
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have"]` {
t.Errorf("the manifest reads %s: the vault reads the controller's assignments and the haves alone", r)
}
sources, _ := filepath.Glob("*.go")
bucket := regexp.MustCompile(`stdio\.State\(|nats-io|jetstream|KeyValue`)
@@ -164,6 +165,12 @@ func TestNoBucketHoldsASecret(t *testing.T) {
if f == "accounts.go" {
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Put(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Keys(", "", 1))
}
// The haves, in main.go alone: one read and one watch, never a write.
if f == "main.go" {
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(HaveRead).Watch(", "", 1))
}
if m := bucket.Find(code); m != nil {
t.Errorf("%s opens a bucket (%s): the vault keeps every secret in its own store only", f, m)
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-vault
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376
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
+4
View File
@@ -1,5 +1,9 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865 h1:VLir9NZbyi1LdH1YwWDIVRuL+lDC+AYlm6FZPlvmlm4=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261021100209-cb54756c3865/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936 h1:HGBoWimZXYZp+Qn9VCstYDitOP84t9EUQYd1yM9j2eg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026102838-174789941936/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376 h1:by2zBASLb8h5jA1IiYJWykXwLnHYpuZh/zFNa4NsbOU=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261026112527-c2a410bb6376/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=
+3 -1
View File
@@ -18,7 +18,8 @@
"account-hashed"
],
"reads": [
"mesh-controller.assignments"
"mesh-controller.assignments",
"node-engine.have"
],
"state": [
{
@@ -154,6 +155,7 @@
"secret_handed",
"secret_own",
"account_hash",
"account_adopt",
"secret_asks",
"secret_vault_export",
"provisioner_retirement",