package identity import ( "crypto/ed25519" "crypto/rand" "encoding/base64" "fmt" "os" "path/filepath" "strings" ) // The key a node serves TLS with, on its name inside the mesh. // // A fourth key, and the reasoning is the one this file's neighbours already give twice: **a key // used for two purposes is one rotation away from breaking the other**. The identity key signs // messages to the mesh and would do for TLS — Ed25519 works in TLS 1.3 — and reusing it would // mean rotating a node's identity every time its certificate is replaced, or the reverse. // // **The private half never leaves the machine.** The mesh is told the public half at enrolment // and signs a certificate binding it to this node's internal name, which is the whole of what a // certificate authority does. There is no request to send and nothing to seal: the mesh issues // something public, about a key it cannot use. // // novox/hq 08-connectivity: the mesh CA certifies internal names, and it is not a bootstrap // concern — a joining node verifies the control plane against the fingerprint in its token, so // nothing needs the CA before membership. // ServingKey is an Ed25519 keypair a node presents when something connects to it by name. type ServingKey struct { // Public is what the mesh records and certifies. Public string `json:"public"` // Private never leaves this machine. Private string `json:"private"` } // GenerateServingKey makes this node's key for serving on its internal name. func GenerateServingKey() (ServingKey, error) { public, private, err := ed25519.GenerateKey(rand.Reader) if err != nil { return ServingKey{}, fmt.Errorf("cannot generate this node's serving key: %w", err) } return ServingKey{ Public: base64.StdEncoding.EncodeToString(public), Private: base64.StdEncoding.EncodeToString(private), }, nil } // ServingKeyPath is where the private half lives. // // A file of its own, named by whatever configuration needs it — the same arrangement the overlay // key has, and for the same reason: the mesh can compose a service's configuration without ever // holding the key that configuration points at. func ServingKeyPath(statePath string) string { return dirOf(statePath) + "/serving.key" } // CertificatePath is where the certificate the mesh issued lives. // // Beside the key, and written by the host from an ordinary declaration — it is public, so it // travels in the open like any other file. func CertificatePath(statePath string) string { return dirOf(statePath) + "/serving.crt" } // LoadServingKey reads this node's serving key. // // It does not make one, for the same reason LoadSealingKey does not: a key the mesh has never // certified is a key nothing will trust, so a node that quietly generated one would serve a // certificate for a key it no longer has and fail in a way that names neither. func LoadServingKey(path string) (ServingKey, error) { raw, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return ServingKey{}, fmt.Errorf( "this node has no serving key at %s, so nothing can be certified for it — it is "+ "made at enrolment, and a node that joined before had none", path) } return ServingKey{}, err } private, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(raw))) if err != nil || len(private) != ed25519.PrivateKeySize { return ServingKey{}, fmt.Errorf("%s is not a serving key", path) } key := ed25519.PrivateKey(private) return ServingKey{ Public: base64.StdEncoding.EncodeToString(key.Public().(ed25519.PublicKey)), Private: base64.StdEncoding.EncodeToString(private), }, nil } // WriteServingKey puts the private half where configuration can point at it. func WriteServingKey(path string, key ServingKey) error { if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { return err } return os.WriteFile(path, []byte(key.Private+"\n"), 0o600) }