diff --git a/internal/identity/identity.go b/internal/identity/identity.go index eb48920..9ff2ee8 100644 --- a/internal/identity/identity.go +++ b/internal/identity/identity.go @@ -28,6 +28,9 @@ func Path(statePath string) string { return filepath.Join(filepath.Dir(statePath), FileName) } +// dirOf is where a node keeps everything it knows about itself. +func dirOf(statePath string) string { return filepath.Dir(statePath) } + // Identity is this node's own keypair, the name the mesh knows it by, and what it needs to get // back to that mesh without a person. // @@ -44,6 +47,10 @@ type Identity struct { Private []byte `json:"private"` Membership Membership `json:"membership"` + + // Overlay is this node's key on the private network. Generated here, like the identity above, + // and for the same reason: the mesh computes a graph it cannot impersonate. + Overlay OverlayKey `json:"overlay"` } // Membership is how this node reaches the mesh it belongs to, and who it believes. diff --git a/internal/identity/overlay.go b/internal/identity/overlay.go new file mode 100644 index 0000000..4c3eb39 --- /dev/null +++ b/internal/identity/overlay.go @@ -0,0 +1,54 @@ +package identity + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/base64" + "fmt" +) + +// The node's key on the private network, which is a different key from the one that says who it +// is — and deliberately so. +// +// novox/hq 08-connectivity: each node generates its own keypair, the private half never leaves +// the machine, and the public half is published to the mesh. That means the control plane +// computes a peer graph it cannot itself impersonate: it knows every public key and holds no +// private one, so it can say who may talk to whom without being able to pretend to be any of them. +// +// Separate from the identity keypair because they are verified by different things at different +// times — the identity signs messages to the mesh, this one encrypts traffic between nodes — and +// a key used for two purposes is one rotation away from breaking the other. + +// OverlayKey is a Curve25519 keypair, which is what WireGuard uses. +type OverlayKey struct { + // Public is what travels. Base64, which is the form WireGuard configuration files use, so it + // is carried the way it will be written rather than converted at the last moment. + Public string `json:"public"` + + // Private never leaves this machine. It is written to a file of its own that the interface + // configuration points at, so the control plane can compose that configuration without ever + // holding this. + Private string `json:"private"` +} + +// GenerateOverlayKey makes this node's keypair for the private network. +func GenerateOverlayKey() (OverlayKey, error) { + private, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return OverlayKey{}, fmt.Errorf("cannot generate this node's overlay key: %w", err) + } + return OverlayKey{ + Public: base64.StdEncoding.EncodeToString(private.PublicKey().Bytes()), + Private: base64.StdEncoding.EncodeToString(private.Bytes()), + }, nil +} + +// OverlayKeyPath is where the private half lives: a file of its own, referenced by the interface +// configuration rather than embedded in it. +// +// That separation is what lets the mesh compose the configuration. WireGuard's `PostUp` can set a +// private key from a file, so the declaration the control plane sends names this path and carries +// no secret — and the file it names was written by the node, from a key nothing else ever saw. +func OverlayKeyPath(statePath string) string { + return dirOf(statePath) + "/overlay.key" +}