diff --git a/internal/identity/authority.go b/internal/identity/authority.go index 209d0ca..171e30b 100644 --- a/internal/identity/authority.go +++ b/internal/identity/authority.go @@ -117,6 +117,25 @@ func (i *Identity) Certify(ctx context.Context, node, name, servingKey string) ( 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 @@ -147,7 +166,34 @@ func (i *Identity) Certify(ctx context.Context, node, name, servingKey string) ( if err != nil { return "", err } - return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), nil + 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) { diff --git a/internal/identity/authority_test.go b/internal/identity/authority_test.go index c8c2ca0..d14cae0 100644 --- a/internal/identity/authority_test.go +++ b/internal/identity/authority_test.go @@ -204,3 +204,48 @@ func TestTwoProcessesStartingTogetherAgreeOnOneAuthority(t *testing.T) { t.Errorf("%d authorities exist; exactly one may", count) } } + +// Asking twice gives the same certificate, to the byte. +// +// Every signing carries a fresh random serial, so a mesh that signed per composition composed a +// different declaration each 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 but for one serial. +func TestACertificateIsIssuedOnceAndKept(t *testing.T) { + ident := fresh(t) + ctx := context.Background() + public, _ := aServingKey(t) + + if _, err := ident.RecordNodeKey(ctx, "1b7e0000-0000-4000-8000-000000000001", make(ed25519.PublicKey, ed25519.PublicKeySize)); err != nil { + t.Fatal(err) + } + if err := ident.RecordServingKey(ctx, "1b7e0000-0000-4000-8000-000000000001", public); err != nil { + t.Fatal(err) + } + + first, err := ident.Certify(ctx, "a", "a.internal", public) + if err != nil { + t.Fatal(err) + } + second, err := ident.Certify(ctx, "a", "a.internal", public) + if err != nil { + t.Fatal(err) + } + if first != second { + t.Fatal("two askings gave two certificates; every composition then differs by a serial " + + "and a machine carrying one is eternally behind") + } + + // And a new key is a new world: the kept answer must not outlive what it certifies. + fresh2, _ := aServingKey(t) + if err := ident.RecordServingKey(ctx, "1b7e0000-0000-4000-8000-000000000001", fresh2); err != nil { + t.Fatal(err) + } + third, err := ident.Certify(ctx, "a", "a.internal", fresh2) + if err != nil { + t.Fatal(err) + } + if third == first { + t.Fatal("the node rejoined with a new key and was handed the certificate of its old one") + } +}