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~` 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) } }