Every signing carries a fresh random serial, so a mesh that signed per composition composed a different declaration every time it was asked what a machine should be. Every machine carrying a certificate then stood eternally "waiting" — pushed seconds ago and already behind — and the forge test, the first to wait for settledness on such a machine, failed four runs in a row wearing three other faults' clothes. Found live on a kept mesh, which is what settled it: two plans seconds apart, identical to the byte but for one serial, in the certificate file. Deduction had four theories; the diff had one line. The keeping columns had existed since the serving key's migration — "and what was issued for it" — and were written by nothing, the same shape ReleasePorts was found in this morning. Kept beside the serving key it certifies, and it stands while the name, the key and the clock agree: a node rejoining with a new key or renamed gets a fresh signing, exactly as if nothing were kept, and so does one whose certificate is into its last stretch of life. The port's rule and the secret's, applied to the third thing composed fresh each time.
233 lines
8.6 KiB
Go
233 lines
8.6 KiB
Go
package identity
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// The authority that certifies names inside the mesh.
|
|
//
|
|
// novox/hq 08-connectivity keeps two authorities apart on purpose: a public one issues for names
|
|
// the outside world reaches, and this one for names only the mesh knows. **It certifies a public
|
|
// key a node generated**, which is the whole of what a certificate authority does — so nothing
|
|
// secret travels, nothing is sealed, and a copy of this context's store certifies nothing it did
|
|
// not already certify.
|
|
//
|
|
// It is not a bootstrap concern. A joining node verifies the control plane against the fingerprint
|
|
// in its token (ADR 0004), so nothing needs this before membership.
|
|
|
|
// forever is how long an internal certificate lasts.
|
|
//
|
|
// Long, and that is a choice rather than laziness. A short life needs something that renews it,
|
|
// and a renewal that fails silently is a mesh that stops trusting itself on a date nobody wrote
|
|
// down. What makes an internal certificate replaceable is that the mesh can reissue it on demand
|
|
// and the node is told in the ordinary way — not that it expires.
|
|
const forever = 10 * 365 * 24 * time.Hour
|
|
|
|
// Authority is the mesh's own certificate authority.
|
|
type Authority struct {
|
|
Certificate string
|
|
private ed25519.PrivateKey
|
|
}
|
|
|
|
// EstablishAuthority makes the mesh's authority if it has none, and returns it either way.
|
|
//
|
|
// Idempotent like the signing key beside it: two authorities and nothing says which certificate to
|
|
// believe, so the row is written once and read forever after.
|
|
func (i *Identity) EstablishAuthority(ctx context.Context) (Authority, error) {
|
|
held, err := i.authority(ctx)
|
|
if err == nil {
|
|
return held, nil
|
|
}
|
|
if !errors.Is(err, pgx.ErrNoRows) {
|
|
return Authority{}, err
|
|
}
|
|
|
|
public, private, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return Authority{}, err
|
|
}
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
return Authority{}, err
|
|
}
|
|
template := &x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{CommonName: "the mesh"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(forever),
|
|
IsCA: true,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
|
// No BasicConstraintsValid path length: this signs leaves and nothing else, and an
|
|
// authority that could sign another authority is one that can be delegated without
|
|
// anybody deciding to.
|
|
BasicConstraintsValid: true,
|
|
MaxPathLen: 0,
|
|
MaxPathLenZero: true,
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, public, private)
|
|
if err != nil {
|
|
return Authority{}, err
|
|
}
|
|
certificate := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
|
|
|
|
// Written once. A second insert loses to the first, and both callers then read the same
|
|
// authority — which is what must happen when two control planes start together.
|
|
if _, err := i.store.Pool().Exec(ctx,
|
|
`insert into authority (singleton, certificate, private) values (true, $1, $2)
|
|
on conflict (singleton) do nothing`,
|
|
certificate, base64.StdEncoding.EncodeToString(private)); err != nil {
|
|
return Authority{}, err
|
|
}
|
|
return i.authority(ctx)
|
|
}
|
|
|
|
func (i *Identity) authority(ctx context.Context) (Authority, error) {
|
|
var certificate, private string
|
|
if err := i.store.Pool().QueryRow(ctx,
|
|
`select certificate, private from authority where singleton`).Scan(&certificate, &private); err != nil {
|
|
return Authority{}, err
|
|
}
|
|
raw, err := base64.StdEncoding.DecodeString(private)
|
|
if err != nil || len(raw) != ed25519.PrivateKeySize {
|
|
return Authority{}, fmt.Errorf("the mesh's authority key is unusable")
|
|
}
|
|
return Authority{Certificate: certificate, private: ed25519.PrivateKey(raw)}, nil
|
|
}
|
|
|
|
// Certify issues a certificate for a node's internal name, binding the key that node generated.
|
|
//
|
|
// **The public key is given, never made here.** A certificate authority's whole job is to say
|
|
// *this name belongs to the holder of this key*, and an authority that made the key would be
|
|
// saying something about a key it also holds.
|
|
func (i *Identity) Certify(ctx context.Context, node, name, servingKey string) (string, error) {
|
|
public, err := base64.StdEncoding.DecodeString(servingKey)
|
|
if err != nil || len(public) != ed25519.PublicKeySize {
|
|
return "", fmt.Errorf("%s presented something that is not a serving key", node)
|
|
}
|
|
|
|
// **Issued once and kept** — the port's rule and the secret's, applied to the certificate.
|
|
// Every signing carries a fresh random serial, so a mesh that signed per composition
|
|
// composed a different declaration every time it was asked what a machine should be — and
|
|
// every machine carrying a certificate stood eternally "waiting", pushed seconds ago and
|
|
// already behind. Found live on a kept mesh: two plans seconds apart, identical to the byte
|
|
// but for one serial. The columns for keeping it had existed since the serving key's
|
|
// migration — "and what was issued for it" — and were written by nothing, which is the same
|
|
// shape ReleasePorts was found in.
|
|
var kept *string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select certificate from node_key where serving_key = $1 and revoked is null`,
|
|
servingKey).Scan(&kept)
|
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
return "", err
|
|
}
|
|
if kept != nil && stillStands(*kept, name, public) {
|
|
return *kept, nil
|
|
}
|
|
|
|
authority, err := i.EstablishAuthority(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parent, err := parse(authority.Certificate)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
template := &x509.Certificate{
|
|
SerialNumber: serial,
|
|
Subject: pkix.Name{CommonName: name},
|
|
// The name is in the subject alternative names, which is the only place anything has
|
|
// looked for a decade — a certificate carrying it only in the common name is a
|
|
// certificate every modern client refuses.
|
|
DNSNames: []string{name},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(forever),
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
|
}
|
|
der, err := x509.CreateCertificate(rand.Reader, template, parent,
|
|
ed25519.PublicKey(public), authority.private)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
issued := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
|
|
// Kept beside the key it certifies. A serving key nothing recorded keeps nothing, and is
|
|
// certified fresh each time — which only a test does.
|
|
if _, err := i.store.Pool().Exec(ctx,
|
|
`update node_key set certificate = $2, certified_at = now()
|
|
where serving_key = $1 and revoked is null`, servingKey, issued); err != nil {
|
|
return "", err
|
|
}
|
|
return issued, nil
|
|
}
|
|
|
|
// stillStands says whether a kept certificate is still the one Certify would issue: same name,
|
|
// same key, and enough life left that nothing downstream will meet its expiry. Any mismatch means
|
|
// the world moved — the node rejoined with a new key, or its name changed — and the answer is a
|
|
// fresh signing, exactly as if nothing were kept.
|
|
func stillStands(kept, name string, public []byte) bool {
|
|
parsed, err := parse(kept)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if len(parsed.DNSNames) != 1 || parsed.DNSNames[0] != name {
|
|
return false
|
|
}
|
|
held, ok := parsed.PublicKey.(ed25519.PublicKey)
|
|
if !ok || !held.Equal(ed25519.PublicKey(public)) {
|
|
return false
|
|
}
|
|
return time.Until(parsed.NotAfter) > forever/10
|
|
}
|
|
|
|
func parse(certificate string) (*x509.Certificate, error) {
|
|
block, _ := pem.Decode([]byte(certificate))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("the mesh's authority is not a certificate")
|
|
}
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
// RecordServingKey keeps the public half a node generated for serving TLS.
|
|
func (i *Identity) RecordServingKey(ctx context.Context, node, key string) error {
|
|
if key == "" {
|
|
return nil
|
|
}
|
|
_, err := i.store.Pool().Exec(ctx,
|
|
`update node_key set serving_key = $2 where node = $1 and revoked is null`, node, key)
|
|
return err
|
|
}
|
|
|
|
// ServingKeyOf is what a node serves TLS with, empty if it has said nothing.
|
|
func (i *Identity) ServingKeyOf(ctx context.Context, node string) (string, error) {
|
|
var key *string
|
|
err := i.store.Pool().QueryRow(ctx,
|
|
`select serving_key from node_key where node = $1 and revoked is null`, node).Scan(&key)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if key == nil {
|
|
return "", nil
|
|
}
|
|
return *key, nil
|
|
}
|