322 lines
13 KiB
Go
322 lines
13 KiB
Go
package main
|
|
|
|
// The move of the controller's secrets into the vault (design 57's V3, issue 676, the operator's decision of
|
|
// 2026-10-14): each machine's node-engine opens what was sealed to it and hands it in, sealed to a key the vault
|
|
// made for the call, keeping where it came from and its generation; the vault takes that step only from the
|
|
// node-engine of a machine the value was sealed to, never replaces a value it holds, and answers what it kept
|
|
// read back from its own store, sealed to a key the node-engine sent. The controller copies its record of asks
|
|
// as it is. Nothing is rotated.
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/nacl/box"
|
|
)
|
|
|
|
// aReadBackKey is a key the node-engine makes for one read-back: its public half to send, its private to open.
|
|
func aReadBackKey(t *testing.T) (string, *[32]byte, *[32]byte) {
|
|
t.Helper()
|
|
pub, priv, err := box.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(pub[:]), pub, priv
|
|
}
|
|
|
|
// openReadBack opens what the vault read back, as the node-engine does.
|
|
func openReadBack(t *testing.T, sealed string, pub, priv *[32]byte) string {
|
|
t.Helper()
|
|
blob, err := base64.StdEncoding.DecodeString(sealed)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, ok := box.OpenAnonymous(nil, blob, pub, priv)
|
|
if !ok {
|
|
t.Fatal("the read-back does not open with the key sent for it")
|
|
}
|
|
return string(out)
|
|
}
|
|
|
|
// moveIn is one node-engine's two steps for one value.
|
|
func moveIn(t *testing.T, v testVault, caller string, m MoveOf, value string) (MovedIn, error) {
|
|
t.Helper()
|
|
opened, err := v.MoveOpen(caller, m)
|
|
if err != nil {
|
|
return MovedIn{}, err
|
|
}
|
|
key, _, _ := aReadBackKey(t)
|
|
return v.MoveTake(caller, opened.Call, sealToCall(t, opened.SealTo, value), key)
|
|
}
|
|
|
|
const movedValue = "Xq3-the-superusers-password-made-in-2025"
|
|
|
|
func superuser() MoveOf {
|
|
return MoveOf{Node: "anchor", Module: "postgres", Name: "superuser-password", Origin: "made",
|
|
Generation: "g-2025-11-02T10:00:00Z-1", MadeAt: time.Date(2025, 11, 2, 10, 0, 0, 0, time.UTC)}
|
|
}
|
|
|
|
// A machine's own value moves as it is: opened in the vault alone, kept with its origin, the controller's
|
|
// generation and when it was made, and read back from the vault's store to the node-engine, sealed to its key.
|
|
func TestANodeEngineMovesInAValueSealedToItsMachineAsItIs(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
opened, err := v.MoveOpen("node.anchor", superuser())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if opened.Secret != "own/anchor/postgres/superuser-password" || opened.SealTo == "" || opened.Call == "" {
|
|
t.Fatalf("opened %+v", opened)
|
|
}
|
|
key, pub, priv := aReadBackKey(t)
|
|
moved, err := v.MoveTake("node.anchor", opened.Call, sealToCall(t, opened.SealTo, movedValue), key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if moved.Name != opened.Secret || moved.Generation != 1 || moved.Unchanged {
|
|
t.Errorf("moved %+v", moved)
|
|
}
|
|
if got := openReadBack(t, moved.ReadBack, pub, priv); got != movedValue {
|
|
t.Errorf("the read-back opens to %q", got)
|
|
}
|
|
rec, held, _ := v.Store.Read(opened.Secret)
|
|
if !held || rec.Origin != OriginMade || rec.ChangedBy != "node.anchor" || rec.MovedFrom == nil ||
|
|
rec.MovedFrom.Generation != "g-2025-11-02T10:00:00Z-1" || rec.MovedFrom.By != "node.anchor" ||
|
|
!rec.MadeAt.Equal(superuser().MadeAt) {
|
|
t.Errorf("the record: %+v moved from %+v", rec, rec.MovedFrom)
|
|
}
|
|
// Kept as every value is: a copy for the operator.
|
|
if got := openOperator(t, v.operatorPublic, v.operatorPrivate, rec.Operator.Sealed); got != movedValue {
|
|
t.Errorf("the operator copy opens to %q", got)
|
|
}
|
|
if strings.Contains(everything(t, v, opened, moved), movedValue) {
|
|
t.Fatal("the value is in something the vault said, answered or wrote in the clear")
|
|
}
|
|
}
|
|
|
|
// Each origin the controller's store holds is kept as the vault's word for it; any other is refused.
|
|
func TestAMovedValueKeepsWhereItCameFrom(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
for origin, want := range map[string]string{"made": OriginMade, "accepted": OriginAccepted, "issued": OriginIssued} {
|
|
m := superuser()
|
|
m.Name, m.Origin = "from-"+origin, origin
|
|
if _, err := moveIn(t, v, "node.anchor", m, "value-"+origin); err != nil {
|
|
t.Fatalf("%s: %v", origin, err)
|
|
}
|
|
rec, _, _ := v.Store.Read("own/anchor/postgres/from-" + origin)
|
|
if rec.Origin != want {
|
|
t.Errorf("%s was kept as %q", origin, rec.Origin)
|
|
}
|
|
}
|
|
m := superuser()
|
|
m.Origin = "invented"
|
|
if _, err := v.MoveOpen("node.anchor", m); err == nil || !strings.Contains(err.Error(), "origin") {
|
|
t.Errorf("an unknown origin: %v", err)
|
|
}
|
|
}
|
|
|
|
// Only the node-engine of a machine the value was sealed to takes the step: not the controller, not a module's
|
|
// tools, not another machine's node-engine.
|
|
func TestOnlyTheNodeEngineOfAMachineTheValueWasSealedToMovesIt(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
for _, caller := range []string{ControllerCaller, "anchor.node-tools", "anchor.mesh-vault", "node.shanks", "",
|
|
"node.", "node.anchor.x"} {
|
|
if _, err := v.MoveOpen(caller, superuser()); err == nil {
|
|
t.Errorf("%q opened a move of anchor's value", caller)
|
|
}
|
|
}
|
|
// A pair credential was sealed to both its ends: either may move it, and nobody else.
|
|
pair := MoveOf{Provider: "anchor", Provision: "database", Consumer: "shanks", ConsumerModule: "gitea",
|
|
Origin: "made", Generation: "g-1"}
|
|
for _, caller := range []string{"node.anchor", "node.shanks"} {
|
|
if _, err := v.MoveOpen(caller, pair); err != nil {
|
|
t.Errorf("%s, an end of the pair, was refused: %v", caller, err)
|
|
}
|
|
}
|
|
if _, err := v.MoveOpen("node.g14", pair); err == nil {
|
|
t.Error("a machine at neither end opened a move of the pair")
|
|
}
|
|
// The second step is the first step's caller's alone.
|
|
opened, err := v.MoveOpen("node.anchor", superuser())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _, _ := aReadBackKey(t)
|
|
if _, err := v.MoveTake("node.shanks", opened.Call, sealToCall(t, opened.SealTo, movedValue), key); err == nil {
|
|
t.Error("another machine's node-engine took the second step")
|
|
}
|
|
if _, held, _ := v.Store.Read("own/anchor/postgres/superuser-password"); held {
|
|
t.Error("a value was kept from the wrong caller")
|
|
}
|
|
}
|
|
|
|
// A pair credential is named by both ends and its local name, so every row of the controller's store is one
|
|
// secret in the vault, kept as it is.
|
|
func TestAPairCredentialMovesUnderTheNameOfItsRow(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
pair := MoveOf{Provider: "anchor", Provision: "database", Consumer: "shanks", ConsumerModule: "gitea",
|
|
Local: "replica", Origin: "accepted", Generation: "g-7"}
|
|
moved, err := moveIn(t, v, "node.shanks", pair, "the-pair-value")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if moved.Name != "pair/anchor/database/shanks/gitea/replica" {
|
|
t.Errorf("named %s", moved.Name)
|
|
}
|
|
pair.Local = ""
|
|
if moved, err = moveIn(t, v, "node.anchor", pair, "the-ordinary-value"); err != nil ||
|
|
moved.Name != "pair/anchor/database/shanks/gitea" {
|
|
t.Errorf("the ordinary one: %+v, %v", moved, err)
|
|
}
|
|
}
|
|
|
|
// The move never replaces a value: the same value again changes nothing (the other end of a pair, or a move run
|
|
// twice), and a different one is refused, so a value the vault holds is never put back to an older one.
|
|
func TestTheMoveNeverReplacesAValueTheVaultHolds(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
pair := MoveOf{Provider: "anchor", Provision: "database", Consumer: "shanks", ConsumerModule: "gitea",
|
|
Origin: "made", Generation: "g-1"}
|
|
if _, err := moveIn(t, v, "node.anchor", pair, "the-pair-value"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opened, err := v.MoveOpen("node.shanks", pair)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, pub, priv := aReadBackKey(t)
|
|
again, err := v.MoveTake("node.shanks", opened.Call, sealToCall(t, opened.SealTo, "the-pair-value"), key)
|
|
if err != nil || !again.Unchanged || again.Generation != 1 {
|
|
t.Fatalf("the other end's same value: %+v, %v", again, err)
|
|
}
|
|
if got := openReadBack(t, again.ReadBack, pub, priv); got != "the-pair-value" {
|
|
t.Errorf("read back %q", got)
|
|
}
|
|
if _, err := v.Rotate(ControllerCaller, "pair/anchor/database/shanks/gitea", "jochen", "a test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := moveIn(t, v, "node.anchor", pair, "the-pair-value"); err == nil ||
|
|
!strings.Contains(err.Error(), "never replaces") {
|
|
t.Fatalf("a move over a value the vault changed since: %v", err)
|
|
}
|
|
rec, _, _ := v.Store.Read("pair/anchor/database/shanks/gitea")
|
|
if rec.Generation != 2 || rec.Origin != OriginMade {
|
|
t.Errorf("the vault's own value was touched: %+v", rec)
|
|
}
|
|
}
|
|
|
|
// A second step that is not sealed to its call, or carries no key to read back to, takes nothing.
|
|
func TestAMoveTakesNothingItCannotOpenOrReadBack(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
opened, err := v.MoveOpen("node.anchor", superuser())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"", "not a key", "-----BEGIN PRIVATE KEY-----"} {
|
|
if _, err := v.MoveTake("node.anchor", opened.Call, sealToCall(t, opened.SealTo, movedValue), key); err == nil {
|
|
t.Errorf("read-back key %q was taken", key)
|
|
}
|
|
}
|
|
// A refused key leaves the call open for the right one.
|
|
other, _ := v.MoveOpen("node.anchor", superuser())
|
|
key, _, _ := aReadBackKey(t)
|
|
if _, err := v.MoveTake("node.anchor", other.Call, "bm90IHNlYWxlZA==", key); err == nil {
|
|
t.Error("a value not sealed to the call was taken")
|
|
}
|
|
if _, held, _ := v.Store.Read("own/anchor/postgres/superuser-password"); held {
|
|
t.Error("something was kept")
|
|
}
|
|
if moved, err := v.MoveTake("node.anchor", opened.Call, sealToCall(t, opened.SealTo, movedValue), key); err != nil ||
|
|
moved.Generation != 1 {
|
|
t.Errorf("the call after a refused key: %+v, %v", moved, err)
|
|
}
|
|
}
|
|
|
|
// The controller's record of asks is copied as it is, once: each ask's opening and its end, who asked and at which
|
|
// desk, and the bounds then count them. From the controller alone.
|
|
func TestTheRecordOfAsksIsCopiedAsItIsOnce(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
now := v.Now()
|
|
ended := now.Add(-30 * time.Second)
|
|
asks := []MovedAsk{
|
|
{ID: 41, Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: "g14/claude-code",
|
|
OpenedAt: now.Add(-50 * time.Minute), EndedAt: &ended, Outcome: "given"},
|
|
{ID: 42, Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: "g14/claude-code",
|
|
OpenedAt: now.Add(-40 * time.Minute), EndedAt: &ended, Outcome: "dismissed"},
|
|
{ID: 43, Node: "anchor", Module: "telegram", Name: "telegram-token", Desk: "laptop", AskedBy: "ace/agent",
|
|
OpenedAt: now.Add(-20 * time.Minute)},
|
|
}
|
|
if _, err := v.MoveAsks("node.anchor", asks); err == nil {
|
|
t.Error("a node-engine copied the record of asks")
|
|
}
|
|
copied, err := v.MoveAsks(ControllerCaller, asks)
|
|
if err != nil || copied.Copied != 3 || copied.Already != 0 {
|
|
t.Fatalf("copied %+v, %v", copied, err)
|
|
}
|
|
again, err := v.MoveAsks(ControllerCaller, asks)
|
|
if err != nil || again.Copied != 0 || again.Already != 3 {
|
|
t.Fatalf("copied again %+v, %v", again, err)
|
|
}
|
|
all, _ := v.Store.Asks(100)
|
|
if len(all) != 5 {
|
|
t.Fatalf("%d lines: %+v", len(all), all)
|
|
}
|
|
opened := map[string]AskRecord{}
|
|
for _, r := range all {
|
|
if r.Event == AskOpened {
|
|
opened[r.ID] = r
|
|
}
|
|
}
|
|
if r := opened["controller-41"]; r.Secret != "own/anchor/telegram/telegram-token" || r.AskedBy != "g14/claude-code" ||
|
|
r.Desk != "laptop" || !r.At.Equal(asks[0].OpenedAt) || r.Caller != ControllerCaller {
|
|
t.Errorf("ask 41: %+v", r)
|
|
}
|
|
// Three asks in the hour, from the controller's record: the vault's bound holds them.
|
|
if _, err := v.AskOpen(ControllerCaller, telegramAsk("g14/claude-code")); err == nil ||
|
|
!strings.Contains(err.Error(), "in the last hour") {
|
|
t.Errorf("an ask after three copied in the hour: %v", err)
|
|
}
|
|
bad := []MovedAsk{{ID: 44, Node: "anchor", Module: "telegram", Name: "x", Desk: "laptop", AskedBy: "a",
|
|
OpenedAt: now, EndedAt: &ended, Outcome: "whatever"}}
|
|
if _, err := v.MoveAsks(ControllerCaller, bad); err == nil {
|
|
t.Error("an ask that ended in no outcome the record knows was copied")
|
|
}
|
|
}
|
|
|
|
// The two steps as the runtime calls the tool: the caller is the one it proved, never an argument.
|
|
func TestMoveInThroughTheTool(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
tool := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".move-in")
|
|
first := map[string]any{"node": "anchor", "module": "postgres", "name": "superuser-password", "origin": "made",
|
|
"generation": "g-1", "made_at": "2025-11-02T10:00:00Z"}
|
|
if _, err := tool.runAs("controller", first); err == nil {
|
|
t.Fatal("the controller opened a move")
|
|
}
|
|
got, err := tool.runAs("node.anchor", first)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opened := got.(Opened)
|
|
key, pub, priv := aReadBackKey(t)
|
|
got, err = tool.runAs("node.anchor", map[string]any{"call": opened.Call,
|
|
"sealed": sealToCall(t, opened.SealTo, movedValue), "read_back_to": key})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if back := openReadBack(t, got.(MovedIn).ReadBack, pub, priv); back != movedValue {
|
|
t.Errorf("read back %q", back)
|
|
}
|
|
rec, _, _ := v.Store.Read("own/anchor/postgres/superuser-password")
|
|
if rec.MadeAt.Year() != 2025 || rec.MovedFrom == nil || rec.MovedFrom.Generation != "g-1" {
|
|
t.Errorf("kept %+v", rec)
|
|
}
|
|
asks := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".move-asks")
|
|
moved, err := asks.runAs("controller", map[string]any{"asks": []any{map[string]any{"id": 7, "node": "anchor",
|
|
"module": "telegram", "name": "telegram-token", "desk": "laptop", "asked_by": "g14/claude-code",
|
|
"opened_at": "2026-10-01T10:00:00Z", "ended_at": "2026-10-01T10:00:20Z", "outcome": "given"}}})
|
|
if err != nil || moved.(AsksMoved).Copied != 1 {
|
|
t.Fatalf("asks through the tool: %+v, %v", moved, err)
|
|
}
|
|
}
|