nox-mesh-host key makes the tunnel key, or reads the one made, and prints its public half for the token to be issued for. enrol with a token that carries a tunnel takes that key, refuses another, writes mesh0 with the hub as its one peer and starts it, then reaches the bus over it (novox/hq ADR 0169). Tokens without a tunnel enrol as before.
91 lines
3.8 KiB
Go
91 lines
3.8 KiB
Go
package identity
|
|
|
|
import (
|
|
"crypto/ecdh"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// OverlayKeyFrom makes this node's overlay key from a private key it did not generate: the found
|
|
// tunnel's, on an adopted node whose private network takes that tunnel over (novox/hq ADR 0105).
|
|
// The one case where the mesh takes a credential it did not mint. From here on it is stored and
|
|
// sealed exactly as a generated one — in the identity file and the key file, readable by root
|
|
// alone — and the mesh receives only the public half, derived here from the private one so the
|
|
// two cannot disagree.
|
|
func OverlayKeyFrom(privateBase64 string) (OverlayKey, error) {
|
|
raw, err := base64.StdEncoding.DecodeString(privateBase64)
|
|
if err != nil {
|
|
return OverlayKey{}, fmt.Errorf("the found tunnel's private key is not base64: %w", err)
|
|
}
|
|
private, err := ecdh.X25519().NewPrivateKey(raw)
|
|
if err != nil {
|
|
return OverlayKey{}, fmt.Errorf("the found tunnel's private key is not a Curve25519 key: %w", err)
|
|
}
|
|
return OverlayKey{
|
|
Public: base64.StdEncoding.EncodeToString(private.PublicKey().Bytes()),
|
|
Private: base64.StdEncoding.EncodeToString(private.Bytes()),
|
|
}, nil
|
|
}
|
|
|
|
// LoadOverlayKey reads the key `key` made and left in its file (novox/hq ADR 0169).
|
|
func LoadOverlayKey(path string) (OverlayKey, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return OverlayKey{}, err
|
|
}
|
|
key, err := OverlayKeyFrom(strings.TrimSpace(string(raw)))
|
|
if err != nil {
|
|
return OverlayKey{}, fmt.Errorf("%s does not hold a tunnel key: %w", path, err)
|
|
}
|
|
return key, 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"
|
|
}
|