package main import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "os" "path/filepath" "testing" "time" ) // writePair mints a throwaway self-signed leaf with the given CN and writes it as // a cert/key PEM pair, so the reloader has a real, loadable x509 key pair. func writePair(t *testing.T, certPath, keyPath, cn string) { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatal(err) } tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: cn}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) if err != nil { t.Fatal(err) } kder, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { t.Fatal(err) } if err := os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: kder}), 0o600); err != nil { t.Fatal(err) } } func leafCN(t *testing.T, r *certReloader) string { t.Helper() c, err := r.GetCertificate(nil) if err != nil { t.Fatal(err) } leaf, err := x509.ParseCertificate(c.Certificate[0]) if err != nil { t.Fatal(err) } return leaf.Subject.CommonName } func TestCertReloaderHotSwap(t *testing.T) { dir := t.TempDir() crt := filepath.Join(dir, "server.crt") key := filepath.Join(dir, "server.key") writePair(t, crt, key, "old") r, err := newCertReloader(crt, key) if err != nil { t.Fatalf("newCertReloader: %v", err) } if cn := leafCN(t, r); cn != "old" { t.Fatalf("initial CN = %q, want old", cn) } // Rotate the pair on disk with a strictly-later modtime — GetCertificate must // pick it up without any explicit reload call (the renewer-rotates-in-place case). writePair(t, crt, key, "new") future := time.Now().Add(2 * time.Second) if err := os.Chtimes(crt, future, future); err != nil { t.Fatal(err) } if cn := leafCN(t, r); cn != "new" { t.Fatalf("post-rotation CN = %q, want new", cn) } } func TestCertReloaderKeepsPreviousOnBadReload(t *testing.T) { dir := t.TempDir() crt := filepath.Join(dir, "server.crt") key := filepath.Join(dir, "server.key") writePair(t, crt, key, "good") r, err := newCertReloader(crt, key) if err != nil { t.Fatal(err) } // Simulate a half-written rotation: cert file replaced with garbage + a newer // modtime. Reload fails; the last good cert must keep serving. if err := os.WriteFile(crt, []byte("not a pem"), 0o644); err != nil { t.Fatal(err) } future := time.Now().Add(2 * time.Second) if err := os.Chtimes(crt, future, future); err != nil { t.Fatal(err) } if cn := leafCN(t, r); cn != "good" { t.Fatalf("after bad reload CN = %q, want good (kept previous)", cn) } }