package link_test import ( "crypto/ed25519" "strings" "testing" "github.com/novox/mesh-controller/internal/identity" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // novox/hq ADR 0105: a hub that enrolled before the mesh knew to take a tunnel over rekeys onto the // found tunnel's key without re-enrolling — which would rotate every key it holds and remake every // credential the mesh sealed to it. The rekey rides in a report and is signed with the node's // identity key; the mesh verifies it against the key it recorded, and refuses one signed by // another key or one already applied. const ( ownKey = "THE-MESHS-OWN-KEY=======================" tunnelKey = "TUNNEL-KEY-the-found-interfaces-public-key=" ) func theTunnel() *link.Tunnel { return &link.Tunnel{Interface: "wg0", Unit: "wg-quick@wg0", Config: "/etc/wireguard/wg0.conf", Port: 51900, Address: "192.0.2.1/24", Range: "192.0.2.0/24", PublicKey: tunnelKey, Peers: []link.TunnelPeer{{PublicKey: "PEER-A=", Address: "192.0.2.2/32"}}} } // anEnrolledHub is a hub the way it stands before the feature: adopted, placed, its overlay key its // own, its identity key recorded — and a mesh holding both stores. func anEnrolledHub(t *testing.T) (link.Enrolment, inventory.Node, ed25519.PrivateKey) { t.Helper() inv := inventory.ForTest(t) ident := identity.ForTest(t) ctx := t.Context() hub, err := inv.AddNodeAs(ctx, "anchor", true) if err != nil { t.Fatal(err) } public, private, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } if _, err := ident.RecordNodeKey(ctx, hub.ID, public); err != nil { t.Fatal(err) } if err := inv.RecordOverlayKey(ctx, hub.ID, ownKey); err != nil { t.Fatal(err) } if err := inv.SetPlace(ctx, "anchor", "anchor.example:51900", "hosting", true, "10.42.0.1"); err != nil { t.Fatal(err) } return link.Enrolment{Inventory: inv, Identity: ident}, hub, private } func TestASignedRekeyMovesTheHubOntoItsTunnel(t *testing.T) { e, hub, private := anEnrolledHub(t) ctx := t.Context() rekey := &link.Rekey{Previous: ownKey, OverlayKey: tunnelKey, Tunnel: theTunnel()} rekey.Proof = ed25519.Sign(private, link.RekeyProof("anchor", ownKey, tunnelKey, theTunnel())) if err := e.Heard(ctx, link.Report{Node: "anchor", Rekey: rekey}); err != nil { t.Fatal(err) } placed, err := e.Inventory.Overlays(ctx) if err != nil || len(placed) != 1 { t.Fatal(placed, err) } if placed[0].Key != tunnelKey || placed[0].Address != "192.0.2.1" { t.Fatalf("the hub is not on the tunnel's key and address: %+v", placed[0]) } tunnel, _, adopted, err := e.Inventory.AdoptedTunnel(ctx) if err != nil || !adopted || tunnel.Range != "192.0.2.0/24" || len(tunnel.Peers) != 1 { t.Fatalf("the tunnel is not adopted after the rekey: %+v %t %v", tunnel, adopted, err) } _ = hub // Replayed, it is stale: the previous key it names is no longer the node's. err = e.Heard(ctx, link.Report{Node: "anchor", Rekey: rekey}) if err == nil || !strings.Contains(err.Error(), "previous overlay key") { t.Fatalf("a replayed rekey was accepted: %v", err) } } func TestARekeySignedByAnotherKeyIsRefusedAndChangesNothing(t *testing.T) { e, _, _ := anEnrolledHub(t) ctx := t.Context() _, stranger, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } rekey := &link.Rekey{Previous: ownKey, OverlayKey: tunnelKey, Tunnel: theTunnel()} rekey.Proof = ed25519.Sign(stranger, link.RekeyProof("anchor", ownKey, tunnelKey, theTunnel())) err = e.Heard(ctx, link.Report{Node: "anchor", Rekey: rekey}) if err == nil || !strings.Contains(err.Error(), "not signed by anchor's identity key") { t.Fatalf("a rekey signed by a stranger was accepted: %v", err) } placed, _ := e.Inventory.Overlays(ctx) if placed[0].Key != ownKey || placed[0].Address != "10.42.0.1" { t.Fatalf("a refused rekey changed the record: %+v", placed[0]) } if _, _, adopted, _ := e.Inventory.AdoptedTunnel(ctx); adopted { t.Fatal("a refused rekey recorded a tunnel") } // And a proof moved to another tunnel — the signature was over one tunnel, the message names // another — does not verify either. moved := &link.Rekey{Previous: ownKey, OverlayKey: tunnelKey, Tunnel: theTunnel()} other := theTunnel() other.Port = 51820 moved.Proof = ed25519.Sign(mustPrivate(t, e, "anchor"), link.RekeyProof("anchor", ownKey, tunnelKey, other)) if err := e.Heard(ctx, link.Report{Node: "anchor", Rekey: moved}); err == nil { t.Fatal("a proof over another tunnel was accepted") } } // mustPrivate is a fresh key recorded as the node's live one, for signing in a test that needs // the node's own signature after the fixture's key is out of scope. func mustPrivate(t *testing.T, e link.Enrolment, node string) ed25519.PrivateKey { t.Helper() public, private, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } n, err := e.Inventory.NodeByName(t.Context(), node) if err != nil { t.Fatal(err) } if _, err := e.Identity.RecordNodeKey(t.Context(), n.ID, public); err != nil { t.Fatal(err) } return private }