package main import ( "context" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" "log" "net/http" "strings" "time" "certigo-addon-backend/providers" "github.com/google/uuid" ) // RenewalInfoResponse enthält die Antwort von Let's Encrypt RenewalInfo API type RenewalInfoResponse struct { SuggestedWindow struct { Start string `json:"start"` End string `json:"end"` } `json:"suggestedWindow"` } // CalculateCertID berechnet die CertID für Let's Encrypt RenewalInfo API // Die CertID setzt sich aus Authority Key Identifier (AKI) und Serial Number zusammen // Format: base64url(AKI).base64url(SerialNumber) func CalculateCertID(certPEM string) (string, error) { // Parse Zertifikat block, _ := pem.Decode([]byte(certPEM)) if block == nil { return "", fmt.Errorf("fehler beim Dekodieren des PEM-Blocks") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return "", fmt.Errorf("fehler beim Parsen des Zertifikats: %v", err) } // Verwende AuthorityKeyId direkt aus dem Zertifikat if len(cert.AuthorityKeyId) == 0 { return "", fmt.Errorf("Authority Key Identifier nicht im Zertifikat gefunden") } // Encoding Referenz: base64.RawURLEncoding entfernt das Padding (=) automatisch encoder := base64.RawURLEncoding // Teil 1: AKI akiPart := encoder.EncodeToString(cert.AuthorityKeyId) // Teil 2: Serial Number // WICHTIG: .Bytes() nutzen, NICHT .String() (was dezimal wäre) oder Hex! serialPart := encoder.EncodeToString(cert.SerialNumber.Bytes()) // Resultat: AKI.SerialNumber mit Punkt getrennt certID := fmt.Sprintf("%s.%s", akiPart, serialPart) return certID, nil } // FetchRenewalInfo ruft die RenewalInfo von einem ACME-Server ab func FetchRenewalInfo(providerID string, certID string) (*RenewalInfoResponse, error) { // Hole ACME-Provider acmeManager := providers.GetACMEManager() provider, exists := acmeManager.GetACMEProvider(providerID) if !exists { return nil, fmt.Errorf("ACME-Provider '%s' nicht gefunden", providerID) } // Hole RenewalInfo URL vom Provider renewalInfoURL := provider.GetRenewalInfoURL() if renewalInfoURL == "" { return nil, fmt.Errorf("ACME-Provider '%s' unterstützt keine RenewalInfo API", providerID) } // Erstelle URL mit CertID url := fmt.Sprintf("%s/%s", renewalInfoURL, certID) // HTTP Request client := &http.Client{ Timeout: 10 * time.Second, } req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("fehler beim Erstellen des Requests: %v", err) } req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("fehler beim Senden des Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("fehler beim Lesen der Response: %v", err) } if resp.StatusCode != http.StatusOK { // Prüfe ob es ein Staging-Zertifikat ist (404 mit spezifischer Fehlermeldung) if resp.StatusCode == http.StatusNotFound { bodyStr := string(body) if strings.Contains(bodyStr, "Authority Key Identifier that did not match a known issuer") { return nil, fmt.Errorf("RenewalInfo nicht verfügbar für Staging-Zertifikate (Status %d)", resp.StatusCode) } } return nil, fmt.Errorf("RenewalInfo API Fehler (Status %d): %s", resp.StatusCode, string(body)) } var renewalInfo RenewalInfoResponse if err := json.Unmarshal(body, &renewalInfo); err != nil { return nil, fmt.Errorf("fehler beim Parsen der Response: %v", err) } return &renewalInfo, nil } // ProcessRenewalInfoForCertificate verarbeitet RenewalInfo für ein Zertifikat // Trennt die Zertifikatskette, berechnet CertID für Leaf, ruft RenewalInfo ab und erstellt Queue-Eintrag func ProcessRenewalInfoForCertificate(certPEM string, certID string, fqdnID string, spaceID string, renewalEnabled bool) error { if !renewalEnabled { log.Printf("RenewalInfo wird übersprungen - renewal_enabled ist für FQDN %s deaktiviert", fqdnID) return nil } // Trenne Zertifikatskette in Leaf und Intermediate leafPEM, _, err := SplitCertificateChain(certPEM) if err != nil { return fmt.Errorf("fehler beim Trennen der Zertifikatskette: %v", err) } if leafPEM == "" { return fmt.Errorf("kein Leaf-Zertifikat in der Kette gefunden") } // Berechne CertID für Leaf-Zertifikat certIDBase64, err := CalculateCertID(leafPEM) if err != nil { return fmt.Errorf("fehler beim Berechnen der CertID: %v", err) } log.Printf("CertID berechnet: %s für Zertifikat %s", certIDBase64, certID) // Speichere CertID in DB ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() _, err = db.ExecContext(ctx, "UPDATE certificates SET cert_id_base64 = ? WHERE id = ?", certIDBase64, certID) if err != nil { log.Printf("Warnung: Fehler beim Speichern der CertID in DB: %v", err) // Weiter mit RenewalInfo-Abfrage, auch wenn DB-Update fehlschlägt } // Rufe RenewalInfo ab // TODO: ACME-Provider-ID aus Zertifikat/FQDN-Konfiguration holen // Standardmäßig verwenden wir Let's Encrypt Staging acmeProviderID := "letsencrypt-staging" renewalInfo, err := FetchRenewalInfo(acmeProviderID, certIDBase64) if err != nil { // Prüfe ob es ein Staging-Zertifikat ist (RenewalInfo nicht verfügbar) if strings.Contains(err.Error(), "Staging-Zertifikate") { log.Printf("RenewalInfo wird übersprungen - Staging-Zertifikat (CertID: %s)", certIDBase64) return nil // Kein Fehler, einfach überspringen } return fmt.Errorf("fehler beim Abrufen der RenewalInfo: %v", err) } log.Printf("RenewalInfo erhalten: Suggested Window Start: %s, End: %s", renewalInfo.SuggestedWindow.Start, renewalInfo.SuggestedWindow.End) // Parse Suggested Window Start scheduledAt, err := time.Parse(time.RFC3339, renewalInfo.SuggestedWindow.Start) if err != nil { return fmt.Errorf("fehler beim Parsen des Scheduled-At-Datums: %v", err) } // Speichere renewal_scheduled_at in DB scheduledAtStr := scheduledAt.UTC().Format("2006-01-02 15:04:05") ctx, cancel = context.WithTimeout(context.Background(), time.Second*10) defer cancel() _, err = db.ExecContext(ctx, "UPDATE certificates SET renewal_scheduled_at = ? WHERE id = ?", scheduledAtStr, certID) if err != nil { log.Printf("Warnung: Fehler beim Speichern von renewal_scheduled_at in DB: %v", err) } // Erstelle Queue-Eintrag queueID := uuid.New().String() createdAt := time.Now().UTC().Format("2006-01-02 15:04:05") ctx, cancel = context.WithTimeout(context.Background(), time.Second*10) defer cancel() _, err = db.ExecContext(ctx, ` INSERT INTO renewal_queue (id, certificate_id, fqdn_id, space_id, scheduled_at, status, created_at) VALUES (?, ?, ?, ?, ?, 'pending', ?) `, queueID, certID, fqdnID, spaceID, scheduledAtStr, createdAt) if err != nil { return fmt.Errorf("fehler beim Erstellen des Queue-Eintrags: %v", err) } log.Printf("RenewalInfo verarbeitet und Queue-Eintrag erstellt (ID: %s, Scheduled: %s)", queueID, scheduledAtStr) return nil }