110 lines
4.0 KiB
Go
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)
|
|
}
|
|
}
|