08-connectivity keeps two authorities apart on purpose: a public one for names the outside world reaches, and the mesh's own for names only the mesh knows. Nothing implemented the second, so anything between machines was plaintext or trust-on-first-use — which the design refuses everywhere else. A node now generates a fourth key at enrolment and reports the public half. A fourth, because 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, and reusing it would mean rotating a node's identity every time its certificate is replaced. **Nothing secret travels and nothing is sealed.** A certificate authority says "this name belongs to the holder of this key", so the mesh signs a public half it cannot use, and the certificate it issues is public. A module asks for one and is given the certificate and, if it wants, the mesh's own — the private key is a path to a file the machine already has, the same arrangement the private network's key uses. Asserted by verifying rather than inspecting, because a certificate that parses and does not chain fails at the moment something connects: - what the mesh issues verifies against the mesh, for the name asked for - the name is in the subject alternative names, since a certificate carrying it only in the common name is refused by every modern client - it certifies the key the node generated and no other - another mesh's certificate does not verify, which is the whole point of two authorities being separate - the authority cannot sign another authority — one that could is one that can be delegated without anybody deciding to - two control planes starting together agree on one authority, or a mesh has certificates half its machines refuse Certificates last ten years, which is a choice: a short life needs something to renew it, and a renewal that fails silently is a mesh that stops trusting itself on a date nobody wrote down. What makes one replaceable is that the mesh reissues on demand, not that it expires.
187 lines
6.6 KiB
Go
187 lines
6.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)
|
|
}
|
|
|
|
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
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), nil
|
|
}
|
|
|
|
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
|
|
}
|