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