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.
207 lines
6.1 KiB
Go
207 lines
6.1 KiB
Go
package identity
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/pem"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// The authority that certifies names inside the mesh.
|
|
//
|
|
// Asserted by verifying, not by inspecting: a certificate that parses and does not chain is a
|
|
// certificate that fails at the moment something connects, which is the worst place to find out.
|
|
|
|
func aServingKey(t *testing.T) (public string, private ed25519.PrivateKey) {
|
|
t.Helper()
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return base64.StdEncoding.EncodeToString(pub), priv
|
|
}
|
|
|
|
func parsed(t *testing.T, certificate string) *x509.Certificate {
|
|
t.Helper()
|
|
block, _ := pem.Decode([]byte(certificate))
|
|
if block == nil {
|
|
t.Fatal("not a certificate")
|
|
}
|
|
got, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func TestACertificateChainsToTheMeshsOwnAuthority(t *testing.T) {
|
|
ident := fresh(t)
|
|
ctx := context.Background()
|
|
public, _ := aServingKey(t)
|
|
|
|
certificate, err := ident.Certify(ctx, "workstation", "workstation.internal", public)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
authority, err := ident.EstablishAuthority(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
roots := x509.NewCertPool()
|
|
if !roots.AppendCertsFromPEM([]byte(authority.Certificate)) {
|
|
t.Fatal("the mesh's authority is not usable as a root")
|
|
}
|
|
if _, err := parsed(t, certificate).Verify(x509.VerifyOptions{
|
|
Roots: roots, DNSName: "workstation.internal",
|
|
}); err != nil {
|
|
t.Fatalf("what the mesh issued does not verify against the mesh: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheNameIsWhereEverythingLooksForIt(t *testing.T) {
|
|
// A certificate carrying the name only in its common name is one every modern client refuses.
|
|
ident := fresh(t)
|
|
public, _ := aServingKey(t)
|
|
certificate, err := ident.Certify(context.Background(), "a", "a.internal", public)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := parsed(t, certificate)
|
|
if len(got.DNSNames) != 1 || got.DNSNames[0] != "a.internal" {
|
|
t.Fatalf("the name is not in the subject alternative names: %v", got.DNSNames)
|
|
}
|
|
}
|
|
|
|
func TestItCertifiesTheKeyTheNodeGeneratedAndNoOther(t *testing.T) {
|
|
// A certificate authority's whole job is to say "this name belongs to the holder of this
|
|
// key". One that made the key would be saying something about a key it also holds.
|
|
ident := fresh(t)
|
|
public, private := aServingKey(t)
|
|
certificate, err := ident.Certify(context.Background(), "a", "a.internal", public)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
inside, ok := parsed(t, certificate).PublicKey.(ed25519.PublicKey)
|
|
if !ok {
|
|
t.Fatalf("the certificate carries a %T", parsed(t, certificate).PublicKey)
|
|
}
|
|
if !inside.Equal(private.Public()) {
|
|
t.Fatal("the certificate is for a key the node does not hold")
|
|
}
|
|
}
|
|
|
|
func TestAnAuthorityIsEstablishedOnceAndKept(t *testing.T) {
|
|
// Two authorities and nothing says which certificate to believe.
|
|
ident := fresh(t)
|
|
ctx := context.Background()
|
|
first, err := ident.EstablishAuthority(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := ident.EstablishAuthority(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Certificate != second.Certificate {
|
|
t.Fatal("asking twice made a second authority")
|
|
}
|
|
}
|
|
|
|
func TestSomethingThatIsNotAServingKeyIsRefused(t *testing.T) {
|
|
ident := fresh(t)
|
|
for _, bad := range []string{"", "not-base64!", base64.StdEncoding.EncodeToString([]byte("short"))} {
|
|
if _, err := ident.Certify(context.Background(), "a", "a.internal", bad); err == nil {
|
|
t.Fatalf("%q was certified", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheAuthorityCannotBeUsedToMakeAnotherAuthority(t *testing.T) {
|
|
// An authority that could sign another is one that can be delegated without anybody deciding
|
|
// to. The path length says it cannot.
|
|
ident := fresh(t)
|
|
authority, err := ident.EstablishAuthority(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := parsed(t, authority.Certificate)
|
|
if !got.IsCA {
|
|
t.Fatal("the authority is not an authority")
|
|
}
|
|
if got.MaxPathLen != 0 || !got.MaxPathLenZero {
|
|
t.Fatalf("the authority may sign another authority: path length %d", got.MaxPathLen)
|
|
}
|
|
}
|
|
|
|
func TestACertificateFromAnotherMeshDoesNotVerify(t *testing.T) {
|
|
// The whole point of two authorities being separate: one mesh's certificate means nothing to
|
|
// another, and the check that says so is the one that must not be skipped.
|
|
one, two := fresh(t), fresh(t)
|
|
public, _ := aServingKey(t)
|
|
certificate, err := one.Certify(context.Background(), "a", "a.internal", public)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := two.EstablishAuthority(context.Background())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
roots := x509.NewCertPool()
|
|
roots.AppendCertsFromPEM([]byte(other.Certificate))
|
|
if _, err := parsed(t, certificate).Verify(x509.VerifyOptions{
|
|
Roots: roots, DNSName: "a.internal",
|
|
}); err == nil {
|
|
t.Fatal("another mesh's certificate verified")
|
|
} else if !strings.Contains(err.Error(), "authority") && !strings.Contains(err.Error(), "signed") {
|
|
t.Fatalf("refused for the wrong reason: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTwoProcessesStartingTogetherAgreeOnOneAuthority(t *testing.T) {
|
|
// A restart while another copy is coming up. Both find nothing and both generate; only one
|
|
// insert may survive, and the loser must read back the winner rather than return the
|
|
// authority it generated and did not store — a mesh with two authorities has certificates
|
|
// half its machines refuse.
|
|
ident := fresh(t)
|
|
|
|
var wg sync.WaitGroup
|
|
authorities := make([]Authority, 6)
|
|
errs := make([]error, 6)
|
|
for i := range authorities {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
authorities[i], errs[i] = ident.EstablishAuthority(context.Background())
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
for i, err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("establish %d failed: %v", i, err)
|
|
}
|
|
}
|
|
for i, a := range authorities {
|
|
if a.Certificate != authorities[0].Certificate {
|
|
t.Errorf("establish %d has a different authority from establish 0", i)
|
|
}
|
|
}
|
|
|
|
var count int
|
|
if err := ident.store.Pool().QueryRow(t.Context(),
|
|
`select count(*) from authority`).Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("%d authorities exist; exactly one may", count)
|
|
}
|
|
}
|