package main import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "os" "path/filepath" "testing" "time" ) // certRemainingValidity entscheidet, ob ueberhaupt erneuert wird — ein // falsches Ergebnis heisst entweder "Zertifikat laeuft unbemerkt ab" // (genau der Befund auf utm-2) oder "wir erneuern bei jedem Tick". func writeTestPEM(t *testing.T, dir, name string, notAfter time.Time, withKey bool) string { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("key: %v", err) } tmpl := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: name}, NotBefore: time.Now().Add(-time.Hour), NotAfter: notAfter, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) if err != nil { t.Fatalf("cert: %v", err) } var buf []byte // Reihenfolge wie certstore.WriteCombined: erst Cert(-Kette), dann Key. buf = append(buf, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...) if withKey { kd, err := x509.MarshalECPrivateKey(key) if err != nil { t.Fatalf("marshal key: %v", err) } buf = append(buf, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: kd})...) } p := filepath.Join(dir, name+".pem") if err := os.WriteFile(p, buf, 0o600); err != nil { t.Fatalf("write: %v", err) } return p } func TestCertRemainingValidity_LongLived(t *testing.T) { dir := t.TempDir() p := writeTestPEM(t, dir, "node.example.com", time.Now().Add(60*24*time.Hour), true) got, err := certRemainingValidity(p) if err != nil { t.Fatalf("unerwarteter Fehler: %v", err) } if got <= mgmtCertRenewThreshold { t.Errorf("60d-Cert muss ueber dem 30d-Schwellwert liegen, got %v", got) } } func TestCertRemainingValidity_ExpiringSoon(t *testing.T) { dir := t.TempDir() p := writeTestPEM(t, dir, "node.example.com", time.Now().Add(5*24*time.Hour), true) got, err := certRemainingValidity(p) if err != nil { t.Fatalf("unerwarteter Fehler: %v", err) } if got > mgmtCertRenewThreshold { t.Errorf("5d-Cert muss unter dem Schwellwert liegen, got %v", got) } } // Der utm-2-Fall: bereits abgelaufen → negative Restlaufzeit, also // eindeutig unter dem Schwellwert und damit erneuerungspflichtig. func TestCertRemainingValidity_AlreadyExpired(t *testing.T) { dir := t.TempDir() p := writeTestPEM(t, dir, "node.example.com", time.Now().Add(-14*24*time.Hour), true) got, err := certRemainingValidity(p) if err != nil { t.Fatalf("unerwarteter Fehler: %v", err) } if got >= 0 { t.Errorf("abgelaufenes Cert muss negative Restlaufzeit liefern, got %v", got) } if got > mgmtCertRenewThreshold { t.Errorf("abgelaufenes Cert muss erneuert werden, got %v", got) } } func TestCertRemainingValidity_MissingFile(t *testing.T) { if _, err := certRemainingValidity(filepath.Join(t.TempDir(), "nope.pem")); err == nil { t.Error("fehlende Datei muss einen Fehler liefern (→ ausstellen)") } } // Nur-Key-Datei: darf nicht als gueltiges Zertifikat durchgehen, sonst // wuerde ein kaputter Zustand nie repariert. func TestCertRemainingValidity_NoCertificateBlock(t *testing.T) { dir := t.TempDir() p := filepath.Join(dir, "keyonly.pem") key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) kd, _ := x509.MarshalECPrivateKey(key) if err := os.WriteFile(p, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: kd}), 0o600); err != nil { t.Fatalf("write: %v", err) } if _, err := certRemainingValidity(p); err == nil { t.Error("PEM ohne CERTIFICATE-Block muss einen Fehler liefern") } }