A node generates the key it serves TLS with
A fourth key, reported at enrolment like the others. 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 private half never leaves the machine. The mesh is told the public half and signs a certificate binding it to this node's name inside the mesh — so there is nothing to seal, and a copy of what the mesh holds certifies nothing it did not already certify. It does not make one on demand, for the same reason the sealing key 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.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user