// Package identity is the context that holds who anything in the mesh is. // // novox/hq ADR 0006 names it as one of the seven. Built second, and only as far as the control // plane's own signing identity — what a *node* presents to prove it is that node is not decided // anywhere, and this deliberately stops short of guessing at it. // // It owns its store exclusively (novox/hq ADR 0008): a database called `identity`, reached with a // credential no other context holds — including `inventory`, in the same process. package identity import ( "context" "crypto/ed25519" "crypto/sha256" "embed" "encoding/hex" "errors" "fmt" "time" "github.com/jackc/pgx/v5" "github.com/novox/mesh-controller/internal/store" ) // Name is what this context is called: its database and its credential are named after it. const Name = "identity" //go:embed migrations/*.sql var files embed.FS // Migrations are this context's schema changes, in order. func Migrations() ([]store.Migration, error) { return store.LoadMigrations(files, "migrations") } // Identity is this context, holding the store it exclusively owns. type Identity struct{ store *store.Store } // Open connects to the identity store. func Open(ctx context.Context) (*Identity, error) { s, err := store.Open(ctx, Name) if err != nil { return nil, err } return &Identity{store: s}, nil } func (i *Identity) Close() { i.store.Close() } // Ready waits for the database to answer. func (i *Identity) Ready(ctx context.Context, within time.Duration) error { return i.store.Ready(ctx, within) } // SigningKey is the control plane's signing identity. Public is what travels in a token. type SigningKey struct { ID string Public ed25519.PublicKey Created time.Time } // Fingerprint is how a person compares two keys without reading 32 bytes. // // Of the public half, which is the half anything else ever sees. func (k SigningKey) Fingerprint() string { sum := sha256.Sum256(k.Public) return hex.EncodeToString(sum[:]) } // ErrNoSigningKey means this control plane has never generated one. var ErrNoSigningKey = errors.New("this control plane has no signing key") // Active is the key currently signing. // // Absence is an error rather than an empty key. A control plane that cannot find its signing // identity must say so: signing with nothing, or with a freshly invented key, would produce // declarations that every existing node correctly refuses — and the refusal would look like a // compromise rather than a missing file. func (i *Identity) Active(ctx context.Context) (SigningKey, error) { var k SigningKey var public []byte err := i.store.Pool().QueryRow(ctx, `select id, public, created from signing_key where retired is null`). Scan(&k.ID, &public, &k.Created) if errors.Is(err, pgx.ErrNoRows) { return SigningKey{}, ErrNoSigningKey } if err != nil { return SigningKey{}, err } k.Public = public return k, nil } // Establish generates the signing identity if there is not one already. // // Idempotent, and it has to be: the control plane runs this at every start, and a second key // generated by a restart would be a mesh whose nodes hold the wrong public half — every // declaration refused, by every node, with nothing having gone wrong that anybody could see. // // The insert is what makes it safe rather than the check before it. Two processes starting // together both find nothing; only one insert survives the partial unique index, and the other // reads back the winner instead of failing. func (i *Identity) Establish(ctx context.Context) (SigningKey, error) { existing, err := i.Active(ctx) if err == nil { return existing, nil } if !errors.Is(err, ErrNoSigningKey) { return SigningKey{}, err } public, private, err := ed25519.GenerateKey(nil) if err != nil { return SigningKey{}, fmt.Errorf("cannot generate a signing key: %w", err) } _, err = i.store.Pool().Exec(ctx, `insert into signing_key (public, private) values ($1, $2) on conflict do nothing`, []byte(public), []byte(private)) if err != nil { return SigningKey{}, err } // Read back rather than return what was generated: on conflict this process generated a key // that was not stored, and returning it would hand out a public half nothing will ever sign // with (novox/hq ADR 0018 — a picture is read from the system). return i.Active(ctx) } // Sign signs a declaration with the active key. // // The private half is fetched per call rather than held in memory for the process's lifetime. // That is not paranoia about memory: it means a key retired while this process runs stops being // used at the next signature rather than at the next restart. func (i *Identity) Sign(ctx context.Context, message []byte) ([]byte, error) { var private []byte err := i.store.Pool().QueryRow(ctx, `select private from signing_key where retired is null`).Scan(&private) if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNoSigningKey } if err != nil { return nil, err } return ed25519.Sign(ed25519.PrivateKey(private), message), nil } // Verify checks a signature against a public key. Here because the host does the same thing with // the same algorithm, and the two must not drift apart. func Verify(public ed25519.PublicKey, message, signature []byte) bool { return ed25519.Verify(public, message, signature) } // NodeKey is the public half of a node's own keypair, as the mesh holds it. type NodeKey struct { ID string Node string Public ed25519.PublicKey Issued time.Time } // ErrNotThisNode is what verification returns when a key is not the live one for a node. // // One error whether the key is unknown, revoked, or belongs to a different node. Whoever is // presenting a key that does not work is either a machine whose operator can be told out of band, // or something probing, and the second must not learn which. var ErrNotThisNode = errors.New("that key does not identify that node") // RecordNodeKey writes down the public key the mesh will believe for a node. // // Any previous key for the node is revoked in the same transaction. Two live identities for one // node record is novox/hq ADR 0004's stolen-laptop case — the machine that was replaced going on // being believed — and the window between two statements is exactly when it would exist. func (i *Identity) RecordNodeKey(ctx context.Context, node string, public ed25519.PublicKey) (NodeKey, error) { if len(public) != ed25519.PublicKeySize { return NodeKey{}, fmt.Errorf( "a node key is %d bytes and this is %d: a node presents an Ed25519 public key", ed25519.PublicKeySize, len(public)) } tx, err := i.store.Pool().Begin(ctx) if err != nil { return NodeKey{}, err } defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }() if _, err := tx.Exec(ctx, `update node_key set revoked = now() where node = $1 and revoked is null`, node); err != nil { return NodeKey{}, err } var k NodeKey var stored []byte err = tx.QueryRow(ctx, `insert into node_key (node, public) values ($1, $2) returning id, node, public, issued`, node, []byte(public)).Scan(&k.ID, &k.Node, &stored, &k.Issued) if err != nil { return NodeKey{}, err } k.Public = stored if err := tx.Commit(ctx); err != nil { return NodeKey{}, err } return k, nil } // LiveKey is the key currently identifying a node. func (i *Identity) LiveKey(ctx context.Context, node string) (NodeKey, error) { var k NodeKey var public []byte err := i.store.Pool().QueryRow(ctx, `select id, node, public, issued from node_key where node = $1 and revoked is null`, node).Scan(&k.ID, &k.Node, &public, &k.Issued) if errors.Is(err, pgx.ErrNoRows) { return NodeKey{}, ErrNotThisNode } if err != nil { return NodeKey{}, err } k.Public = public return k, nil } // VerifyNode checks that something signed a challenge with the live key for a node. // // This is the whole of proving a node is that node, and it is the same operation the node performs // in the other direction on every declaration it receives. Nothing here is stored that could be // replayed: the mesh holds a public key, so a copy of this database proves nothing to anybody. func (i *Identity) VerifyNode(ctx context.Context, node string, challenge, signature []byte) error { key, err := i.LiveKey(ctx, node) if err != nil { return err } if !ed25519.Verify(key.Public, challenge, signature) { return ErrNotThisNode } return nil }