The vault adopts the hashes of the bus accounts it holds no value for from the bus node's handing (ADR 0547 clause 9, issue 1945)
take-over waits on a hash for every user of the loaded list, and the vault holds no value for the controller's own account, the node accounts and the enrolled devices. The bus node's engine now hands every loaded user's hash in the bus module's have, sealed to the vault's key (mesh-host and mesh-sdk on the same branch). The vault answers its key in node-key, reads the haves (manifest reads node-engine.have, granted from the manifest, no controller change), watches the bus module's have, opens the handing with its own key, and writes each user it holds no value for into bus-accounts marked adopted, with the list and node it came from, saying account-hashed. It never touches a user whose covered account it holds or whose key is its own hash, writes an adopted key again only for another hash, and refuses a handing from any node the assignments do not name as holding the bus module. Tools: account_adopt adopts now from the have as the watch does; secret_own, unnarrowed by module, lists the adopted accounts and the last adoption.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
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 adopted with nothing refused
|
||||
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 adoption
|
||||
// took whole.
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(a.Refused) == 0 {
|
||||
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.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()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
@@ -50,10 +51,43 @@ 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
|
||||
}
|
||||
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)
|
||||
if _, err := env.Vault.AdoptFrom(node, c.Value, "the watch of "+HaveRead, false); err != nil {
|
||||
say("adopted nothing from %s: %v", c.Key, err)
|
||||
}
|
||||
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 +141,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()
|
||||
|
||||
@@ -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,98 @@ 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"} {
|
||||
accounts = append(accounts, delivery.HandedAccount{User: u, Hash: handing[u]})
|
||||
}
|
||||
handed, err := delivery.HandAccounts(accounts, sealedTo, "sha256:"+handing["node.ace"][7:15], 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"])
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -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.20261026102838-174789941936
|
||||
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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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=
|
||||
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=
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user