Files
mesh-controller/internal/identity/identity.go
T
jschoubben c3b88b9148 Rename mesh-control -> mesh-controller, substrate -> foundation
One name per thing, per the HQ glossary: the module/container/image/binary/repo
becomes mesh-controller, the seat the-controller, and the store+broker pair the
foundation (embedded base bundles, default template and example lock renamed with
their go:embed directives). No behaviour change — a pure vocabulary rename.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-16 18:40:40 +02:00

240 lines
8.2 KiB
Go

// 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
}