Files
mesh-controller/internal/link/enrol_bus_credential_test.go
T

110 lines
4.0 KiB
Go

//go:build integration
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)
}
}