package link_test import ( "crypto/ed25519" "crypto/rand" "testing" "time" "golang.org/x/crypto/bcrypt" "github.com/novox/mesh-controller/internal/identity" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // What a node is given to come back with, on the bus being built. // // **The credential becomes usable at the next composition, not when it is made**, which is the one // real difference from the bus the mesh runs on today: there a management call makes it live at once. // So what has to be true here is that the mesh recorded it and told the node, and the rest is a push. // aMeshReadyToEnrol is both stores with a signing key established, which a control plane does at // start: one that cannot sign is one whose declarations every node correctly refuses. func aMeshReadyToEnrol(t *testing.T) (*inventory.Inventory, *identity.Identity) { t.Helper() inv := inventory.ForTest(t) ident := identity.ForTest(t) if _, err := ident.Establish(t.Context()); err != nil { t.Fatal(err) } return inv, ident } func aTokenFor(t *testing.T, inv *inventory.Inventory, node string) (string, ed25519.PublicKey) { t.Helper() ctx := t.Context() if _, err := inv.AddNode(ctx, node); err != nil { t.Fatal(err) } issued, err := inv.IssueToken(ctx, node, time.Hour) if err != nil { t.Fatal(err) } public, _, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } return issued.Secret, public } // A node enrolling onto the bus being built is told a password of its own, and the mesh keeps only // its hash — which is what the next composition writes into the bus's user list. func TestANodeEnrollingOnTheNewBusIsMintedACredentialTheMeshOnlyHashes(t *testing.T) { inv, ident := aMeshReadyToEnrol(t) ctx := t.Context() secret, public := aTokenFor(t, inv, "anchor") reply, err := link.Enrolment{Inventory: inv, Identity: ident, OnNATS: true}.Enrol(ctx, link.EnrolRequest{ Node: "anchor", Secret: secret, PublicKey: public}) if err != nil { t.Fatal(err) } if reply.Password == "" { t.Fatal("the node was told no password, so it keeps a one-time secret as a credential") } if reply.Password == secret { t.Fatal("the node was handed the token's own secret back: a credential that lives for years " + "must not be the string that was pasted into a terminal") } // Recorded under the name the composed file will use, and as a hash: a credential recoverable // from the mesh's store is one whose blast radius is the store's. hash, known, err := inv.BusUserHash(ctx, "node.anchor") if err != nil || !known { t.Fatalf("the mesh kept no credential for the node it enrolled: %v %v", known, err) } if hash == reply.Password { t.Fatal("the store holds the password itself") } if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(reply.Password)); err != nil { t.Fatalf("what the mesh kept does not verify what it told the node: %v", err) } } // On the bus the mesh runs on today, with no management configured, nothing is minted and the node is // told so by being given no password — it keeps the token's secret, which it says out loud. // // **This is the check that the switch is a switch.** A node enrolling on one bus must not come away // with a credential for the other: it would be half-moved, and nothing anywhere would say which half. func TestANodeEnrollingOnTheOldBusIsMintedNoCredentialForTheNewOne(t *testing.T) { inv, ident := aMeshReadyToEnrol(t) ctx := t.Context() secret, public := aTokenFor(t, inv, "anchor") reply, err := link.Enrolment{Inventory: inv, Identity: ident}.Enrol(ctx, link.EnrolRequest{ Node: "anchor", Secret: secret, PublicKey: public}) if err != nil { t.Fatal(err) } if reply.Password != "" { t.Fatalf("a node on the old bus was given a password from nowhere: %q", reply.Password) } if _, known, err := inv.BusUserHash(ctx, "node.anchor"); err != nil || known { t.Fatalf("a node enrolling on the old bus was given a credential for the new one: %v %v", known, err) } }