mesh-vault (mesh-catalog f3b13de2) answers a move it refuses with `refused: {reason, held}` and `why`, and
no read-back. The engine read only the read-back, so every such row failed "answered with no read-back"
and the controller asked it again on every pass.
Quick fix: treat any answer without a read-back as done. Thorough fix (chosen): read `refused`. For
holds-another-value the vault's value is the live one: the move answers done, superseded, at the held
generation with the held record in its words, so the controller records the row and stops asking; the
journal says it once per secret. Any other reason fails in the vault's own words.
162 lines
6.2 KiB
Go
162 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
)
|
|
|
|
// standInVault answers move-in's two steps as the vault does: a key made for the call, the value opened with it,
|
|
// and the value kept read back sealed to the key the second step sent. lie makes its read-back another value.
|
|
type standInVault struct {
|
|
t *testing.T
|
|
pub *[32]byte
|
|
priv *[32]byte
|
|
opened map[string]any
|
|
kept string
|
|
lie string
|
|
refuse string
|
|
// answer replaces the second step's answer: the vault's refusal as fields (mesh-catalog f3b13de2).
|
|
answer map[string]any
|
|
carried []string
|
|
}
|
|
|
|
func (v *standInVault) ask(verb string, args any) (json.RawMessage, error) {
|
|
raw, _ := json.Marshal(args)
|
|
v.carried = append(v.carried, string(raw))
|
|
if verb != "move-in" {
|
|
v.t.Fatalf("asked %s", verb)
|
|
}
|
|
if v.refuse != "" {
|
|
return nil, errors.New(v.refuse)
|
|
}
|
|
var a map[string]any
|
|
_ = json.Unmarshal(raw, &a)
|
|
if a["call"] == nil {
|
|
v.opened = a
|
|
pub, priv, _ := box.GenerateKey(rand.Reader)
|
|
v.pub, v.priv = pub, priv
|
|
return json.Marshal(map[string]string{"call": "c1", "seal_to": base64.StdEncoding.EncodeToString(pub[:]),
|
|
"secret": "own/anchor/postgres/superuser-password"})
|
|
}
|
|
blob, _ := base64.StdEncoding.DecodeString(a["sealed"].(string))
|
|
value, ok := box.OpenAnonymous(nil, blob, v.pub, v.priv)
|
|
if !ok {
|
|
v.t.Fatal("what the engine sealed does not open with the call's key")
|
|
}
|
|
v.kept = string(value)
|
|
if v.answer != nil {
|
|
return json.Marshal(v.answer)
|
|
}
|
|
back := v.kept
|
|
if v.lie != "" {
|
|
back = v.lie
|
|
}
|
|
key, _ := base64.StdEncoding.DecodeString(a["read_back_to"].(string))
|
|
var to [32]byte
|
|
copy(to[:], key)
|
|
boxed, _ := box.SealAnonymous(nil, []byte(back), &to, rand.Reader)
|
|
return json.Marshal(map[string]any{"name": "own/anchor/postgres/superuser-password", "generation": 1,
|
|
"read_back": base64.StdEncoding.EncodeToString(boxed)})
|
|
}
|
|
|
|
const theValue = "the-superusers-password"
|
|
|
|
func aMoveOf(t *testing.T, key identity.SealingKey) link.MoveSecret {
|
|
t.Helper()
|
|
sealed, err := identity.Seal(key.Public, []byte(theValue))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return link.MoveSecret{Node: "anchor", By: "the move", MoveSecretAsk: link.MoveSecretAsk{Module: "postgres",
|
|
Name: "superuser-password", Origin: "made", Generation: "g-1", Sealed: sealed, SealedTo: key.Public}}
|
|
}
|
|
|
|
// The engine opens what was sealed to its machine, hands it to the vault sealed to the call's key — never in the
|
|
// clear — and answers what the vault kept only once the read-back opens to the same value.
|
|
func TestTheEngineMovesAValueSealedToItAndChecksTheReadBack(t *testing.T) {
|
|
key, err := identity.GenerateSealingKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
vault := &standInVault{t: t}
|
|
moved, err := moveIntoTheVault(vault.ask, key, aMoveOf(t, key))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if moved.Secret != "own/anchor/postgres/superuser-password" || moved.Generation != 1 || vault.kept != theValue {
|
|
t.Fatalf("moved %+v, the vault kept %q", moved, vault.kept)
|
|
}
|
|
if vault.opened["module"] != "postgres" || vault.opened["name"] != "superuser-password" || vault.opened["node"] != "anchor" ||
|
|
vault.opened["origin"] != "made" || vault.opened["generation"] != "g-1" ||
|
|
vault.opened["sealing_key"] != key.Public {
|
|
t.Errorf("the first step named %v", vault.opened)
|
|
}
|
|
if strings.Contains(strings.Join(vault.carried, " "), theValue) {
|
|
t.Fatal("the value crossed the bus in the clear")
|
|
}
|
|
}
|
|
|
|
// A read-back that is not the value handed in is a failed move, said; so is a value this machine cannot open, and
|
|
// the vault's refusal.
|
|
func TestTheEngineSaysAMoveThatDidNotHold(t *testing.T) {
|
|
key, _ := identity.GenerateSealingKey()
|
|
vault := &standInVault{t: t, lie: "something-else"}
|
|
if _, err := moveIntoTheVault(vault.ask, key, aMoveOf(t, key)); err == nil ||
|
|
!strings.Contains(err.Error(), "read back") {
|
|
t.Fatalf("a read-back of another value: %v", err)
|
|
}
|
|
other, _ := identity.GenerateSealingKey()
|
|
if _, err := moveIntoTheVault((&standInVault{t: t}).ask, key, aMoveOf(t, other)); err == nil ||
|
|
!strings.Contains(err.Error(), "sealing key") {
|
|
t.Fatalf("a value sealed to another key: %v", err)
|
|
}
|
|
refusing := &standInVault{t: t, refuse: "the move never replaces a value the vault holds"}
|
|
if _, err := moveIntoTheVault(refusing.ask, key, aMoveOf(t, key)); err == nil ||
|
|
!strings.Contains(err.Error(), "never replaces") {
|
|
t.Fatalf("the vault's refusal: %v", err)
|
|
}
|
|
// The vault's refusal as fields (mesh-catalog f3b13de2, no read-back): for another value held, the vault's is the
|
|
// live one and the move is done, superseded, at the held generation; any other reason fails in the vault's words.
|
|
held := map[string]any{"name": "own/anchor/postgres/superuser-password", "generation": 3,
|
|
"changed_by": "mesh-vault@novox", "changed_at": "2026-10-30T15:50:00Z", "origin": "made"}
|
|
for _, row := range []struct {
|
|
refused map[string]any
|
|
gen int
|
|
fails string
|
|
}{
|
|
{map[string]any{"reason": "holds-another-value", "held": held}, 3, ""},
|
|
{map[string]any{"reason": "locked"}, 0, "the vault refused"},
|
|
} {
|
|
v := &standInVault{t: t, answer: map[string]any{"refused": row.refused, "why": "in words"}}
|
|
moved, err := moveIntoTheVault(v.ask, key, aMoveOf(t, key))
|
|
switch {
|
|
case row.fails != "" && (err == nil || !strings.Contains(err.Error(), row.fails)):
|
|
t.Errorf("refused %v: %v", row.refused["reason"], err)
|
|
case row.fails == "" && (err != nil || moved.Generation != row.gen || moved.Secret != held["name"] ||
|
|
!strings.Contains(moved.Superseded, "changed by mesh-vault@novox")):
|
|
t.Errorf("refused %v: moved %+v, %v", row.refused["reason"], moved, err)
|
|
}
|
|
}
|
|
// A pair credential names both its ends.
|
|
pair := aMoveOf(t, key)
|
|
pair.Module, pair.Name = "", ""
|
|
pair.Provider, pair.Provision, pair.Consumer, pair.ConsumerModule = "anchor", "database", "shanks", "gitea"
|
|
ends := &standInVault{t: t}
|
|
if _, err := moveIntoTheVault(ends.ask, key, pair); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ends.opened["provider"] != "anchor" || ends.opened["consumer"] != "shanks" || ends.opened["consumer_module"] != "gitea" ||
|
|
ends.opened["node"] != nil {
|
|
t.Errorf("the pair's first step named %v", ends.opened)
|
|
}
|
|
}
|