implemented LE and ACME and fixed some bugs
This commit is contained in:
189
backend/renewal_info.go
Normal file
189
backend/renewal_info.go
Normal file
@@ -0,0 +1,189 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user