The mesh certifies names inside it
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.
This commit is contained in:
@@ -0,0 +1,186 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user