Files
mesh-host/internal/link/movesecret_test.go
T
mesh-tools bff1ac14a1
mesh/merge-gate head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head bff1ac14a198 merged into trunk 49c9002c04d3 beside mesh-controller#355@8c381172 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group release/57-config-and-provisioning delivered: every member is delivered
Feature 57 (config and provisioning): release mesh-host, the reviewed release branch on top of main
2026-10-16 08:24:13 +00:00

142 lines
6.8 KiB
Go

package link
import (
"crypto/ed25519"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/nats-io/nats.go"
)
// The move's ask (design 57's V3, issue 676): a hand-over's fields and the row's own in one signed object, under a
// signing context of its own. The controller's side holds the same names and words.
func TestTheMovesAskKeepsItsFieldNamesAndContext(t *testing.T) {
body, _ := json.Marshal(MoveSecretAsk{Module: "m", Name: "n", Provider: "p", Provision: "q", Consumer: "c",
ConsumerModule: "cm", Local: "l", Origin: "made", Generation: "g", MadeAt: time.Unix(1, 0).UTC(),
Sealed: "s", SealedTo: "k"})
if got := keysOf(t, body); got != "consumer consumer_module generation local made_at module name origin provider "+
"provision sealed sealed_to" {
t.Fatalf("the move's fields are %q", got)
}
if AskMoveSecretSubject("ace") != "mesh.node.ace.ask.move-secret" {
t.Fatalf("the subject is %q", AskMoveSecretSubject("ace"))
}
if MoveSecretContext != "novox-mesh move-secret v1\n" {
t.Fatalf("the signing context is %q", MoveSecretContext)
}
answer, _ := json.Marshal(MoveSecretAnswer{Said: "s", Refused: "r", Secret: "own/a/b/c", Generation: 1, Unchanged: true})
if got := keysOf(t, answer); got != "generation refused said secret unchanged" {
t.Fatalf("the answer's fields are %q", got)
}
}
// signedMove is a move's ask as the controller makes it.
func signedMove(t *testing.T, key ed25519.PrivateKey, ask HandOverAsk, move MoveSecretAsk) []byte {
t.Helper()
raw, _ := json.Marshal(struct {
HandOverAsk
MoveSecretAsk
}{ask, move})
body, _ := json.Marshal(SignedHandOver{Ask: raw, Signature: ed25519.Sign(key, append([]byte(MoveSecretContext), raw...))})
return body
}
// A move is believed as every signed ask is, then names one row this machine holds — its own secret, or a pair
// credential with this machine at one end — with a value sealed to it and an origin the store has; only then is the
// handler given it. A hand-over's signature is not a move's.
func TestAMoveIsTakenOnlyForARowThisMachineHolds(t *testing.T) {
m, key := aMember(t)
now := time.Now()
var given []MoveSecret
handle := func(s MoveSecret) (MovedSecret, error) {
given = append(given, s)
return MovedSecret{Secret: "own/" + m.Node + "/postgres/superuser-password", Generation: 1}, nil
}
check := HandOverCheck{Signer: m.Signer, Node: m.Node, Nonces: &Nonces{}}
ask := func(nonce string) HandOverAsk {
return HandOverAsk{Node: m.Node, By: "the move", Expires: now.Add(HandOverGood), Nonce: nonce}
}
own := MoveSecretAsk{Module: "postgres", Name: "superuser-password", Origin: "made", Sealed: "c2VhbGVk"}
for why, body := range map[string][]byte{
"a hand-over's": signedAsk(t, key, ask(aNonce+"01")),
"no value": signedMove(t, key, ask(aNonce+"02"), MoveSecretAsk{Module: "postgres", Name: "x", Origin: "made"}),
"no origin": signedMove(t, key, ask(aNonce+"03"), MoveSecretAsk{Module: "postgres", Name: "x", Sealed: "c2Vh"}),
"both shapes": signedMove(t, key, ask(aNonce+"04"), MoveSecretAsk{Module: "postgres", Name: "x", Provider: m.Node,
Provision: "database", Consumer: "shanks", ConsumerModule: "gitea", Origin: "made", Sealed: "c2Vh"}),
"neither end": signedMove(t, key, ask(aNonce+"05"), MoveSecretAsk{Provider: "anchor", Provision: "database",
Consumer: "shanks", ConsumerModule: "gitea", Origin: "made", Sealed: "c2Vh"}),
"a path": signedMove(t, key, HandOverAsk{Node: m.Node, Path: "/etc", By: "x", Expires: now.Add(HandOverGood),
Nonce: aNonce + "06"}, own),
"two lines": signedMove(t, key, ask(aNonce+"07"), MoveSecretAsk{Module: "postgres", Name: "x\ny", Origin: "made",
Sealed: "c2Vh"}),
} {
if a := AnswerMoveSecret(check, handle, body); a.Refused == "" || !strings.HasSuffix(a.Refused, "nothing was moved") {
t.Errorf("%s: not refused, or refused without saying nothing was moved: %+v", why, a)
}
}
if len(given) != 0 {
t.Fatalf("a refused ask reached the move: %+v", given)
}
a := AnswerMoveSecret(check, handle, signedMove(t, key, ask(aNonce+"08"), own))
if a.Refused != "" || a.Secret != "own/"+m.Node+"/postgres/superuser-password" || a.Generation != 1 || a.Said == "" ||
len(given) != 1 || given[0].Name != "superuser-password" || given[0].By != "the move" {
t.Fatalf("a good ask: %+v %+v", a, given)
}
pair := MoveSecretAsk{Provider: "anchor", Provision: "database", Consumer: m.Node, ConsumerModule: "gitea",
Origin: "accepted", Sealed: "c2Vh"}
if a := AnswerMoveSecret(check, handle, signedMove(t, key, ask(aNonce+"09"), pair)); a.Refused != "" {
t.Fatalf("a pair this machine consumes was refused: %+v", a)
}
// What the move refuses is said, and nothing else.
refusing := func(MoveSecret) (MovedSecret, error) { return MovedSecret{}, errors.New("the vault said no") }
if a := AnswerMoveSecret(check, refusing, signedMove(t, key, ask(aNonce+"10"), own)); !strings.Contains(a.Refused,
"the vault said no") || a.Secret != "" {
t.Fatalf("a refused move: %+v", a)
}
}
// **Against a real server**: the move's signed ask on the node's own subject is answered with what the handler
// moved, and the engine asks the vault on the subject that names it, `node~<machine>` last.
func TestNatsAMoveIsAskedOfTheNodeAndTheVaultIsAskedInItsName(t *testing.T) {
conn, js := aBus(t)
m, key := aMember(t)
vault, err := conn.Subscribe("mesh.seat.mesh-vault.call.move-in.node~"+m.Node, func(msg *nats.Msg) {
_ = msg.Respond([]byte(`{"result":{"call":"c1","seal_to":"k"}}`))
})
if err != nil {
t.Fatal(err)
}
defer vault.Unsubscribe()
l := &natsLink{conn: conn, js: js, node: m.Node, signer: m.Signer}
q := &Queue{Membership: m}
q.attach(t.Context(), l)
l.HearMoveSecrets(func(s MoveSecret) (MovedSecret, error) {
raw, err := q.AskVault(t.Context(), "move-in", map[string]string{"module": s.Module})
if err != nil || !strings.Contains(string(raw), `"call":"c1"`) {
return MovedSecret{}, fmt.Errorf("the vault answered %s, %v", raw, err)
}
return MovedSecret{Secret: "own/" + s.Node + "/" + s.Module + "/" + s.Name, Generation: 1}, nil
})
defer l.Close()
if err := conn.Flush(); err != nil {
t.Fatal(err)
}
ask := HandOverAsk{Node: m.Node, By: "the move", Expires: time.Now().Add(HandOverGood), Nonce: strings.Repeat("ab", 16)}
reply, err := conn.Request(AskMoveSecretSubject(m.Node), signedMove(t, key, ask,
MoveSecretAsk{Module: "postgres", Name: "superuser-password", Origin: "made", Sealed: "c2Vh"}), 5*time.Second)
if err != nil {
t.Fatal(err)
}
var said MoveSecretAnswer
if err := json.Unmarshal(reply.Data, &said); err != nil || said.Secret != "own/"+m.Node+"/postgres/superuser-password" {
t.Fatalf("answered %s, %v", reply.Data, err)
}
if _, err := (&Queue{Membership: Membership{Node: "nowhere"}}).AskVault(t.Context(), "move-in", nil); !errors.Is(err, ErrNotAsked) {
t.Fatalf("a queue with no link asked the vault: %v", err)
}
}