190 lines
6.3 KiB
Go
190 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"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 Let's Encrypt ab
|
|
func FetchRenewalInfo(certID string) (*RenewalInfoResponse, error) {
|
|
// Let's Encrypt RenewalInfo API URL (Staging)
|
|
url := fmt.Sprintf("https://acme-staging-v02.api.letsencrypt.org/acme/renewal-info/%s", 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
|
|
renewalInfo, err := FetchRenewalInfo(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
|
|
}
|