package main import ( "bytes" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "time" ) // ACMEDirectory enthält die Endpunkte eines ACME-Servers type ACMEDirectory struct { NewNonce string `json:"newNonce"` NewAccount string `json:"newAccount"` NewOrder string `json:"newOrder"` RevokeCert string `json:"revokeCert"` KeyChange string `json:"keyChange"` Meta struct { TermsOfService string `json:"termsOfService"` Website string `json:"website"` CaaIdentities []string `json:"caaIdentities"` ExternalAccountRequired bool `json:"externalAccountRequired"` } `json:"meta"` } // getACMEDirectory ruft die Directory-Endpunkte von einem ACME-Server ab func getACMEDirectory(directoryURL string) (*ACMEDirectory, error) { client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Get(directoryURL) if err != nil { return nil, fmt.Errorf("fehler beim Abrufen der ACME Directory: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("fehler beim Abrufen der ACME Directory (Status %d): %s", resp.StatusCode, string(body)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("fehler beim Lesen der Directory-Response: %v", err) } var directory ACMEDirectory if err := json.Unmarshal(body, &directory); err != nil { return nil, fmt.Errorf("fehler beim Parsen der Directory-Response: %v", err) } return &directory, nil } // ACMEKeyPair enthält Private und Public Key type ACMEKeyPair struct { PrivateKey *rsa.PrivateKey PublicKey *rsa.PublicKey KeyDir string } // CertificateRequestResult enthält das Ergebnis einer Zertifikatsanfrage type CertificateRequestResult struct { Certificate string `json:"certificate"` PrivateKey string `json:"privateKey"` KeyID string `json:"keyId"` Status []string `json:"status"` OrderURL string `json:"orderUrl,omitempty"` StepStatus map[string]string `json:"stepStatus,omitempty"` // Schritt-Status für Frontend } // loadOrCreateKeyPair lädt ein Key-Paar oder erstellt ein neues func loadOrCreateKeyPair(fqdnID string, keyDir string) (*ACMEKeyPair, error) { // Erstelle Key-Verzeichnis falls nicht vorhanden if err := os.MkdirAll(keyDir, 0755); err != nil { return nil, fmt.Errorf("fehler beim Erstellen des Key-Verzeichnisses: %v", err) } privateKeyPath := filepath.Join(keyDir, fmt.Sprintf("%s_private.pem", fqdnID)) publicKeyPath := filepath.Join(keyDir, fmt.Sprintf("%s_public.pem", fqdnID)) var privateKey *rsa.PrivateKey var err error // Prüfe ob Private Key bereits existiert if _, statErr := os.Stat(privateKeyPath); statErr == nil { // Lade existierenden Key privateKeyData, err := os.ReadFile(privateKeyPath) if err != nil { return nil, fmt.Errorf("fehler beim Lesen des Private Keys: %v", err) } block, _ := pem.Decode(privateKeyData) if block == nil { return nil, fmt.Errorf("fehler beim Dekodieren des Private Keys") } privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("fehler beim Parsen des Private Keys: %v", err) } } else { // Erstelle neuen Key privateKey, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, fmt.Errorf("fehler beim Generieren des Private Keys: %v", err) } // Speichere Private Key privateKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey), }) if err := os.WriteFile(privateKeyPath, privateKeyPEM, 0600); err != nil { return nil, fmt.Errorf("fehler beim Speichern des Private Keys: %v", err) } // Speichere Public Key publicKeyDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { return nil, fmt.Errorf("fehler beim Marshalling des Public Keys: %v", err) } publicKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: publicKeyDER, }) if err := os.WriteFile(publicKeyPath, publicKeyPEM, 0644); err != nil { return nil, fmt.Errorf("fehler beim Speichern des Public Keys: %v", err) } } return &ACMEKeyPair{ PrivateKey: privateKey, PublicKey: &privateKey.PublicKey, KeyDir: keyDir, }, nil } // getNonce ruft einen neuen Nonce vom ACME-Server ab func getNonce(directoryURL string) (string, error) { // Rufe Directory-Endpoint auf, um einen Nonce zu bekommen req, err := http.NewRequest("HEAD", directoryURL, nil) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des HEAD-Requests: %v", err) } client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("fehler beim Senden des HEAD-Requests: %v", err) } defer resp.Body.Close() nonce := resp.Header.Get("Replay-Nonce") if nonce == "" { return "", fmt.Errorf("kein Nonce in Response gefunden") } return nonce, nil } // createJWSForGet erstellt einen JWS für POST-as-GET Requests (leerer Payload) func createJWSForGet(keyPair *ACMEKeyPair, url string, keyID string, nonce string) (string, error) { // Für POST-as-GET ist der Payload leer (nicht {}, sondern wirklich leer) payloadB64 := "" // Leerer String für POST-as-GET // Erstelle Protected Header protected := map[string]interface{}{ "alg": "RS256", "nonce": nonce, "url": url, } if keyID != "" { protected["kid"] = keyID } else { // Für newAccount: verwende JWK (JSON Web Key) jwk, err := publicKeyToJWK(keyPair.PublicKey) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des JWK: %v", err) } protected["jwk"] = jwk } protectedBytes, err := json.Marshal(protected) if err != nil { return "", fmt.Errorf("fehler beim Serialisieren des Protected Headers: %v", err) } protectedB64 := base64.RawURLEncoding.EncodeToString(protectedBytes) // Erstelle Signing Input signingInput := fmt.Sprintf("%s.%s", protectedB64, payloadB64) // Signiere mit Private Key (RS256 = RSA mit SHA-256) hasher := sha256.New() hasher.Write([]byte(signingInput)) hashed := hasher.Sum(nil) signature, err := rsa.SignPKCS1v15(rand.Reader, keyPair.PrivateKey, crypto.SHA256, hashed) if err != nil { return "", fmt.Errorf("fehler beim Signieren: %v", err) } signatureB64 := base64.RawURLEncoding.EncodeToString(signature) // Erstelle JWS jws := map[string]string{ "protected": protectedB64, "payload": payloadB64, "signature": signatureB64, } jwsBytes, err := json.Marshal(jws) if err != nil { return "", fmt.Errorf("fehler beim Serialisieren des JWS: %v", err) } return string(jwsBytes), nil } // createJWS erstellt einen JWS (JSON Web Signature) für ACME-Requests func createJWS(keyPair *ACMEKeyPair, payload interface{}, url string, keyID string, nonce string) (string, error) { // Serialisiere Payload payloadBytes, err := json.Marshal(payload) if err != nil { return "", fmt.Errorf("fehler beim Serialisieren des Payloads: %v", err) } payloadB64 := base64.RawURLEncoding.EncodeToString(payloadBytes) // Erstelle Protected Header protected := map[string]interface{}{ "alg": "RS256", "nonce": nonce, "url": url, } if keyID != "" { protected["kid"] = keyID } else { // Für newAccount: verwende JWK (JSON Web Key) jwk, err := publicKeyToJWK(keyPair.PublicKey) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des JWK: %v", err) } protected["jwk"] = jwk } protectedBytes, err := json.Marshal(protected) if err != nil { return "", fmt.Errorf("fehler beim Serialisieren des Protected Headers: %v", err) } protectedB64 := base64.RawURLEncoding.EncodeToString(protectedBytes) // Erstelle Signing Input signingInput := fmt.Sprintf("%s.%s", protectedB64, payloadB64) // Signiere mit Private Key (RS256 = RSA mit SHA-256) hasher := sha256.New() hasher.Write([]byte(signingInput)) hashed := hasher.Sum(nil) signature, err := rsa.SignPKCS1v15(rand.Reader, keyPair.PrivateKey, crypto.SHA256, hashed) if err != nil { return "", fmt.Errorf("fehler beim Signieren: %v", err) } signatureB64 := base64.RawURLEncoding.EncodeToString(signature) // Erstelle JWS jws := map[string]string{ "protected": protectedB64, "payload": payloadB64, "signature": signatureB64, } jwsBytes, err := json.Marshal(jws) if err != nil { return "", fmt.Errorf("fehler beim Serialisieren des JWS: %v", err) } return string(jwsBytes), nil } // publicKeyToJWK konvertiert einen RSA Public Key zu JWK Format func publicKeyToJWK(pubKey *rsa.PublicKey) (map[string]interface{}, error) { // Berechne n (Modulus) und e (Exponent) für JWK // n muss in Big-Endian Format sein nBytes := pubKey.N.Bytes() n := base64.RawURLEncoding.EncodeToString(nBytes) // e (Exponent) ist normalerweise 65537 (0x10001) oder kleiner // Konvertiere zu Big-Endian Bytes var eBytes []byte if pubKey.E < 256 { eBytes = []byte{byte(pubKey.E)} } else if pubKey.E < 65536 { eBytes = []byte{byte(pubKey.E >> 8), byte(pubKey.E)} } else { // Für größere Exponenten (selten) e := pubKey.E for e > 0 { eBytes = append([]byte{byte(e & 0xFF)}, eBytes...) e >>= 8 } } e := base64.RawURLEncoding.EncodeToString(eBytes) return map[string]interface{}{ "kty": "RSA", "n": n, "e": e, }, nil } // calculateJWKThumbprint berechnet den JWK Thumbprint gemäß RFC 7638 func calculateJWKThumbprint(pubKey *rsa.PublicKey) (string, error) { // Erstelle JWK jwk, err := publicKeyToJWK(pubKey) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des JWK: %v", err) } // Sortiere Keys lexikografisch und erstelle kanonisches JSON (ohne Whitespace) // Für RSA: kty, n, e (in dieser Reihenfolge) canonicalJSON := fmt.Sprintf(`{"e":"%s","kty":"%s","n":"%s"}`, jwk["e"], jwk["kty"], jwk["n"]) // Berechne SHA-256 Hash hasher := sha256.New() hasher.Write([]byte(canonicalJSON)) hash := hasher.Sum(nil) // Base64URL kodieren (ohne Padding) thumbprint := base64.RawURLEncoding.EncodeToString(hash) return thumbprint, nil } // calculateKeyAuthHash berechnet den KeyAuth Hash für DNS-01 Challenge // Formel: keyAuth = token + "." + base64url(sha256(jwk_thumbprint)) // TXT-Record-Wert = base64url(sha256(keyAuth)) func calculateKeyAuthHash(token string, pubKey *rsa.PublicKey) (string, error) { // Phase 1: Berechne JWK Thumbprint thumbprint, err := calculateJWKThumbprint(pubKey) if err != nil { return "", fmt.Errorf("fehler beim Berechnen des JWK Thumbprints: %v", err) } // Phase 2: Erstelle KeyAuth = Token + "." + Thumbprint keyAuth := token + "." + thumbprint // Phase 3: Berechne SHA-256 Hash des KeyAuth hasher := sha256.New() hasher.Write([]byte(keyAuth)) hash := hasher.Sum(nil) // Base64URL kodieren (ohne Padding) txtValue := base64.RawURLEncoding.EncodeToString(hash) return txtValue, nil } // createAccount erstellt einen neuen Account bei einem ACME-Server func createAccount(keyPair *ACMEKeyPair, directoryURL, newAccountURL string, email string, traceID, fqdnID string, statusCallback func(status string)) (string, error) { statusCallback("Erstelle Account bei ACME-Server...") // Hole Nonce vom Server nonce, err := getNonce(directoryURL) if err != nil { return "", fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Erstelle Payload für newAccount payload := map[string]interface{}{ "termsOfServiceAgreed": true, "contact": []string{fmt.Sprintf("mailto:%s", email)}, } // Erstelle JWS (ohne KeyID, da es ein neuer Account ist) jws, err := createJWS(keyPair, payload, newAccountURL, "", nonce) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } // Sende Request req, err := http.NewRequest("POST", newAccountURL, bytes.NewBufferString(jws)) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des HTTP-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("fehler beim Senden des Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("fehler beim Lesen der Response: %v", err) } // Prüfe Status Code if resp.StatusCode != 201 && resp.StatusCode != 200 { return "", fmt.Errorf("fehler bei der Account-Erstellung (Status %d): %s", resp.StatusCode, string(body)) } // Extrahiere KeyID aus Location Header keyID := resp.Header.Get("Location") if keyID == "" { return "", fmt.Errorf("keine KeyID in Location Header gefunden") } statusCallback(fmt.Sprintf("Account erfolgreich erstellt (KeyID: %s)", keyID)) return keyID, nil } // createOrder erstellt eine neue Order bei einem ACME-Server func createOrder(keyPair *ACMEKeyPair, directoryURL, newOrderURL string, keyID string, domains []string, traceID, fqdnID string, statusCallback func(status string)) (string, map[string]interface{}, error) { statusCallback("Erstelle Order bei ACME-Server...") // Hole Nonce vom Server nonce, err := getNonce(directoryURL) if err != nil { return "", nil, fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Erstelle Payload für newOrder payload := map[string]interface{}{ "identifiers": []map[string]string{}, } for _, domain := range domains { payload["identifiers"] = append(payload["identifiers"].([]map[string]string), map[string]string{ "type": "dns", "value": domain, }) } // Erstelle JWS (mit KeyID) jws, err := createJWS(keyPair, payload, newOrderURL, keyID, nonce) if err != nil { return "", nil, fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } // Sende Request req, err := http.NewRequest("POST", newOrderURL, bytes.NewBufferString(jws)) if err != nil { return "", nil, fmt.Errorf("fehler beim Erstellen des HTTP-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } 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) } // Prüfe Status Code if resp.StatusCode != 201 { return "", nil, fmt.Errorf("fehler bei der Order-Erstellung (Status %d): %s", resp.StatusCode, string(body)) } // Parse Response var orderResponse map[string]interface{} if err := json.Unmarshal(body, &orderResponse); err != nil { return "", nil, fmt.Errorf("fehler beim Parsen der Order-Response: %v", err) } orderURL := resp.Header.Get("Location") if orderURL == "" { return "", nil, fmt.Errorf("keine Order URL in Location Header gefunden") } statusCallback(fmt.Sprintf("Order erfolgreich erstellt (URL: %s)", orderURL)) return orderURL, orderResponse, nil } // RequestCertificate beantragt ein Zertifikat von einem ACME-Server // cleanupTokenFunc wird aufgerufen, wenn die Challenge invalid ist, um den Token zu bereinigen // traceID wird vom Aufrufer übergeben, um die gleiche TRACE_ID über den gesamten Prozess zu verwenden func RequestCertificate(ctx *ACMEClientContext, fqdn string, email string, fqdnID string, existingKeyID string, traceID string, updateTokenFunc func(token string) error, cleanupTokenFunc func() error, statusCallback func(status string)) (*CertificateRequestResult, error) { log.Printf("[ACME] ===== REQUEST CERTIFICATE START =====") log.Printf("[ACME] FQDN: %s", fqdn) log.Printf("[ACME] Email: %s", email) log.Printf("[ACME] FQDN ID: %s", fqdnID) log.Printf("[ACME] Existing KeyID: %s", existingKeyID) log.Printf("[ACME] TraceID: %s", traceID) result := &CertificateRequestResult{ Status: []string{}, StepStatus: make(map[string]string), } // Schritt 1: Erstelle/Lade Key-Paar log.Printf("[ACME] Schritt 1: Erstelle/Lade Key-Paar...") statusCallback("Erstelle/Lade Key-Paar...") keyDir := filepath.Join(os.TempDir(), "certigo-keys") result.StepStatus["KEY_PAAR_ERSTELLUNG"] = "loading" keyPair, err := loadOrCreateKeyPair(fqdnID, keyDir) if err != nil { log.Printf("[ACME] FEHLER bei Schritt 1: %v", err) logCertStatus(traceID, fqdnID, "KEY_PAAR_ERSTELLUNG", "FAILED", err.Error()) result.StepStatus["KEY_PAAR_ERSTELLUNG"] = "error" return nil, fmt.Errorf("fehler beim Laden/Erstellen des Key-Paars: %v", err) } log.Printf("[ACME] Schritt 1 erfolgreich: Key-Paar geladen/erstellt") logCertStatus(traceID, fqdnID, "KEY_PAAR_ERSTELLUNG", "OK", "") result.StepStatus["KEY_PAAR_ERSTELLUNG"] = "success" result.Status = append(result.Status, "Key-Paar erfolgreich geladen/erstellt") statusCallback("Key-Paar erfolgreich geladen/erstellt") // Schritt 2: Erstelle Account beim ACME-Server (falls noch nicht vorhanden) log.Printf("[ACME] Schritt 2: Erstelle/Verwende Account...") var keyID string if existingKeyID == "" { log.Printf("[ACME] Keine KeyID vorhanden, erstelle neuen Account...") statusCallback(fmt.Sprintf("Erstelle Account bei %s...", ctx.Provider.GetDisplayName())) result.StepStatus["ACCOUNT_ERSTELLUNG"] = "loading" keyID, err = createAccount(keyPair, ctx.DirectoryURL, ctx.NewAccountURL, email, traceID, fqdnID, statusCallback) if err != nil { log.Printf("[ACME] FEHLER bei Schritt 2 (Account-Erstellung): %v", err) logCertStatus(traceID, fqdnID, "ACCOUNT_ERSTELLUNG", "FAILED", err.Error()) result.StepStatus["ACCOUNT_ERSTELLUNG"] = "error" return nil, fmt.Errorf("fehler bei der Account-Erstellung: %v", err) } log.Printf("[ACME] Schritt 2 erfolgreich: Account erstellt (KeyID: %s)", keyID) logCertStatus(traceID, fqdnID, "ACCOUNT_ERSTELLUNG", "OK", "") result.StepStatus["ACCOUNT_ERSTELLUNG"] = "success" result.KeyID = keyID result.Status = append(result.Status, fmt.Sprintf("Account erfolgreich erstellt (KeyID: %s)", keyID)) } else { log.Printf("[ACME] Verwende existierenden Account (KeyID: %s)", existingKeyID) result.StepStatus["ACCOUNT_ERSTELLUNG"] = "success" keyID = existingKeyID result.KeyID = keyID result.Status = append(result.Status, fmt.Sprintf("Verwende existierenden Account (KeyID: %s)", keyID)) statusCallback(fmt.Sprintf("Verwende existierenden Account (KeyID: %s)", keyID)) } // Schritt 3: Erstelle Order beim ACME-Server baseFqdn := fqdn if strings.HasPrefix(baseFqdn, "*.") { baseFqdn = baseFqdn[2:] } log.Printf("[ACME] Schritt 3: Erstelle Order für Domain: %s", baseFqdn) statusCallback(fmt.Sprintf("Erstelle Order bei %s...", ctx.Provider.GetDisplayName())) result.StepStatus["ORDER_ERSTELLUNG"] = "loading" orderURL, orderResponse, err := createOrder(keyPair, ctx.DirectoryURL, ctx.NewOrderURL, keyID, []string{baseFqdn}, traceID, fqdnID, statusCallback) if err != nil { log.Printf("[ACME] FEHLER bei Schritt 3 (Order-Erstellung): %v", err) logCertStatus(traceID, fqdnID, "ORDER_ERSTELLUNG", "FAILED", err.Error()) result.StepStatus["ORDER_ERSTELLUNG"] = "error" return nil, fmt.Errorf("fehler bei der Order-Erstellung: %v", err) } log.Printf("[ACME] Schritt 3 erfolgreich: Order erstellt (URL: %s)", orderURL) logCertStatus(traceID, fqdnID, "ORDER_ERSTELLUNG", "OK", "") result.StepStatus["ORDER_ERSTELLUNG"] = "success" result.OrderURL = orderURL result.Status = append(result.Status, fmt.Sprintf("Order erfolgreich erstellt (URL: %s)", orderURL)) // Schritt 4: Extrahiere Token aus Order-Response und sende an certigo-acmeproxy if orderResponse != nil { statusCallback("Extrahiere Challenge-Token...") token, err := extractTokenFromOrder(ctx, keyPair, keyID, orderResponse, baseFqdn) if err != nil { logCertStatus(traceID, fqdnID, "TOKEN_EXTRAKTION", "FAILED", err.Error()) return nil, fmt.Errorf("fehler beim Extrahieren des Tokens: %v", err) } if token != "" { // Berechne keyAuth Hash für DNS-01 Challenge keyAuthHash, err := calculateKeyAuthHash(token, keyPair.PublicKey) if err != nil { logCertStatus(traceID, fqdnID, "KEYAUTH_BERECHNUNG", "FAILED", err.Error()) return nil, fmt.Errorf("fehler beim Berechnen des keyAuth Hash: %v", err) } statusCallback("Sende Token an certigo-acmeproxy...") // Sende Token via updateTokenFunc (certigo-acmeproxy erwartet den keyAuth Hash, nicht nur den Token) result.StepStatus["REGISTER_AUFRUF"] = "loading" if err := updateTokenFunc(keyAuthHash); err != nil { logCertStatus(traceID, fqdnID, "REGISTER_AUFRUF", "FAILED", err.Error()) result.StepStatus["REGISTER_AUFRUF"] = "error" return nil, fmt.Errorf("fehler beim Senden des Tokens: %v", err) } logCertStatus(traceID, fqdnID, "REGISTER_AUFRUF", "OK", "") result.StepStatus["REGISTER_AUFRUF"] = "success" result.Status = append(result.Status, "Token erfolgreich an certigo-acmeproxy gesendet") statusCallback("Token erfolgreich an certigo-acmeproxy gesendet") // Schritt 5: Aktiviere Challenge bei Let's Encrypt statusCallback("Aktiviere Challenge bei Let's Encrypt...") challengeURL, err := extractChallengeURLFromOrder(ctx, keyPair, keyID, orderResponse, baseFqdn) if err != nil { logCertStatus(traceID, fqdnID, "CHALLENGE_URL_EXTRAKTION", "FAILED", err.Error()) return nil, fmt.Errorf("fehler beim Extrahieren der Challenge-URL: %v", err) } result.StepStatus["CHALLENGE_AKTIVIERUNG"] = "loading" if err := activateChallenge(ctx, keyPair, keyID, challengeURL, traceID, fqdnID); err != nil { logCertStatus(traceID, fqdnID, "CHALLENGE_AKTIVIERUNG", "FAILED", err.Error()) result.StepStatus["CHALLENGE_AKTIVIERUNG"] = "error" return nil, fmt.Errorf("fehler beim Aktivieren der Challenge: %v", err) } logCertStatus(traceID, fqdnID, "CHALLENGE_AKTIVIERUNG", "OK", "") result.StepStatus["CHALLENGE_AKTIVIERUNG"] = "success" result.Status = append(result.Status, "Challenge bei Let's Encrypt aktiviert") statusCallback("Challenge bei Let's Encrypt aktiviert") // Schritt 6: Warte auf Challenge-Validierung (Polling) statusCallback("Warte auf Challenge-Validierung...") if err := waitForChallengeValidation(ctx, keyPair, keyID, challengeURL, traceID, fqdnID, statusCallback, cleanupTokenFunc); err != nil { // Cleanup wurde bereits in waitForChallengeValidation durchgeführt // Bereinige auch den Token aus der Datenbank if cleanupTokenFunc != nil { if cleanupErr := cleanupTokenFunc(); cleanupErr != nil { } else { } } // Gebe die Fehlermeldung weiter, damit der Benutzer informiert wird return nil, fmt.Errorf("fehler bei der Challenge-Validierung: %v", err) } result.Status = append(result.Status, "Challenge erfolgreich validiert") statusCallback("Challenge erfolgreich validiert") // Schritt 7: Finalisiere Order und hole Zertifikat statusCallback("Finalisiere Order und hole Zertifikat...") result.StepStatus["ZERTIFIKAT_ERSTELLUNG"] = "loading" certPEM, keyPEM, err := finalizeOrderAndGetCertificate(ctx, keyPair, keyID, orderURL, orderResponse, traceID, fqdnID, statusCallback) if err != nil { logCertStatus(traceID, fqdnID, "ZERTIFIKAT_ERSTELLUNG", "FAILED", err.Error()) result.StepStatus["ZERTIFIKAT_ERSTELLUNG"] = "error" return nil, fmt.Errorf("fehler beim Finalisieren der Order: %v", err) } logCertStatus(traceID, fqdnID, "ZERTIFIKAT_ERSTELLUNG", "OK", "") result.StepStatus["ZERTIFIKAT_ERSTELLUNG"] = "success" result.Certificate = certPEM result.PrivateKey = keyPEM result.Status = append(result.Status, "Zertifikat erfolgreich erstellt") statusCallback("Zertifikat erfolgreich erstellt") } else { } } return result, nil } // extractTokenFromOrder extrahiert den Challenge-Token aus der Order-Response func extractTokenFromOrder(ctx *ACMEClientContext, keyPair *ACMEKeyPair, keyID string, orderResponse map[string]interface{}, domain string) (string, error) { // Extrahiere authorizations Array authorizations, ok := orderResponse["authorizations"].([]interface{}) if !ok { return "", fmt.Errorf("keine authorizations in Order-Response gefunden") } if len(authorizations) == 0 { return "", fmt.Errorf("authorizations Array ist leer") } // Hole Nonce für Authorization-Request nonce, err := getNonce(ctx.DirectoryURL) if err != nil { return "", fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Durchlaufe alle Authorizations for _, authURL := range authorizations { authURLStr, ok := authURL.(string) if !ok { continue } // Debug: Curl-Befehl für manuelle Abfrage // Erstelle JWS für Authorization-Request (POST-as-GET: leerer Payload als String "") // Für POST-as-GET muss der Payload leer sein, nicht {} jws, err := createJWSForGet(keyPair, authURLStr, keyID, nonce) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des JWS für Authorization: %v", err) } // Sende POST-as-GET Request für Authorization req, err := http.NewRequest("POST", authURLStr, bytes.NewBufferString(jws)) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des Authorization-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("fehler beim Senden des Authorization-Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("fehler beim Lesen der Authorization-Response: %v", err) } // Update Nonce für nächsten Request newNonce := resp.Header.Get("Replay-Nonce") if newNonce != "" { nonce = newNonce } if resp.StatusCode != 200 { continue } // Parse Authorization-Response var authResponse map[string]interface{} if err := json.Unmarshal(body, &authResponse); err != nil { continue } // Extrahiere Challenges challenges, ok := authResponse["challenges"].([]interface{}) if !ok { continue } // Suche DNS-01 Challenge for _, challenge := range challenges { challengeMap, ok := challenge.(map[string]interface{}) if !ok { continue } challengeType, ok := challengeMap["type"].(string) if !ok || challengeType != "dns-01" { continue } // Extrahiere Token token, ok := challengeMap["token"].(string) if !ok || token == "" { continue } return token, nil } } return "", fmt.Errorf("kein DNS-01 Challenge Token in Authorizations gefunden") } // extractChallengeURLFromOrder extrahiert die Challenge-URL aus der Order-Response func extractChallengeURLFromOrder(ctx *ACMEClientContext, keyPair *ACMEKeyPair, keyID string, orderResponse map[string]interface{}, domain string) (string, error) { // Extrahiere authorizations Array authorizations, ok := orderResponse["authorizations"].([]interface{}) if !ok { return "", fmt.Errorf("keine authorizations in Order-Response gefunden") } if len(authorizations) == 0 { return "", fmt.Errorf("authorizations Array ist leer") } // Hole Nonce für Authorization-Request nonce, err := getNonce(ctx.DirectoryURL) if err != nil { return "", fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Durchlaufe alle Authorizations for _, authURL := range authorizations { authURLStr, ok := authURL.(string) if !ok { continue } // Erstelle POST-as-GET Request für Authorization jws, err := createJWSForGet(keyPair, authURLStr, keyID, nonce) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des JWS für Authorization: %v", err) } req, err := http.NewRequest("POST", authURLStr, bytes.NewBufferString(jws)) if err != nil { return "", fmt.Errorf("fehler beim Erstellen des Authorization-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("fehler beim Senden des Authorization-Requests: %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { continue } body, err := io.ReadAll(resp.Body) if err != nil { continue } // Update Nonce newNonce := resp.Header.Get("Replay-Nonce") if newNonce != "" { nonce = newNonce } // Parse Authorization-Response var authResponse map[string]interface{} if err := json.Unmarshal(body, &authResponse); err != nil { continue } // Extrahiere Challenges challenges, ok := authResponse["challenges"].([]interface{}) if !ok { continue } // Suche DNS-01 Challenge for _, challenge := range challenges { challengeMap, ok := challenge.(map[string]interface{}) if !ok { continue } challengeType, ok := challengeMap["type"].(string) if !ok || challengeType != "dns-01" { continue } // Extrahiere Challenge URL challengeURL, ok := challengeMap["url"].(string) if !ok || challengeURL == "" { continue } // Debug: Curl-Befehl für manuelle Abfrage return challengeURL, nil } } return "", fmt.Errorf("keine DNS-01 Challenge URL in Authorizations gefunden") } // activateChallenge aktiviert eine Challenge beim ACME-Server func activateChallenge(ctx *ACMEClientContext, keyPair *ACMEKeyPair, keyID string, challengeURL string, traceID, fqdnID string) error { // Hole Nonce nonce, err := getNonce(ctx.DirectoryURL) if err != nil { return fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Erstelle Payload für Challenge-Aktivierung (leerer Payload) emptyPayload := map[string]interface{}{} jws, err := createJWS(keyPair, emptyPayload, challengeURL, keyID, nonce) if err != nil { return fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } // Sende POST Request zur Challenge-Aktivierung req, err := http.NewRequest("POST", challengeURL, bytes.NewBufferString(jws)) if err != nil { return fmt.Errorf("fehler beim Erstellen des Challenge-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return fmt.Errorf("fehler beim Senden des Challenge-Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("fehler beim Lesen der Challenge-Response: %v", err) } if resp.StatusCode != 200 { return fmt.Errorf("fehler bei der Challenge-Aktivierung (Status %d): %s", resp.StatusCode, string(body)) } return nil } // cleanupChallenge führt einen Cleanup-Prozess für eine Challenge durch func cleanupChallenge(ctx *ACMEClientContext, keyPair *ACMEKeyPair, keyID string, challengeURL string) error { // Hole Nonce nonce, err := getNonce(ctx.DirectoryURL) if err != nil { return fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Erstelle Payload für Challenge-Cleanup (leerer Payload) // Hinweis: ACME v2 unterstützt kein explizites Cleanup, aber wir können versuchen, // die Challenge-Informationen zu löschen oder zu resetten emptyPayload := map[string]interface{}{} jws, err := createJWS(keyPair, emptyPayload, challengeURL, keyID, nonce) if err != nil { return fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } // Sende POST Request zur Challenge (kann helfen, den Status zu aktualisieren) req, err := http.NewRequest("POST", challengeURL, bytes.NewBufferString(jws)) if err != nil { return fmt.Errorf("fehler beim Erstellen des Cleanup-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return fmt.Errorf("fehler beim Senden des Cleanup-Requests: %v", err) } defer resp.Body.Close() // Cleanup ist optional - auch wenn es fehlschlägt, ist das nicht kritisch // Hauptsache ist, dass der Benutzer informiert wird und von vorne beginnen kann // Lesen der Response ist nicht notwendig für Cleanup io.ReadAll(resp.Body) return nil } // waitForChallengeValidation wartet auf die Validierung der Challenge (Polling) func waitForChallengeValidation(ctx *ACMEClientContext, keyPair *ACMEKeyPair, keyID string, challengeURL string, traceID, fqdnID string, statusCallback func(status string), cleanupTokenFunc func() error) error { maxAttempts := 30 // Maximal 30 Versuche pollInterval := 2 * time.Second // Alle 2 Sekunden prüfen for attempt := 1; attempt <= maxAttempts; attempt++ { statusCallback(fmt.Sprintf("Prüfe Challenge-Status (%d/%d)...", attempt, maxAttempts)) // Hole Nonce nonce, err := getNonce(ctx.DirectoryURL) if err != nil { return fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Erstelle POST-as-GET Request für Challenge-Status jws, err := createJWSForGet(keyPair, challengeURL, keyID, nonce) if err != nil { return fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } // Sende Request req, err := http.NewRequest("POST", challengeURL, bytes.NewBufferString(jws)) if err != nil { return fmt.Errorf("fehler beim Erstellen des Status-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return fmt.Errorf("fehler beim Senden des Status-Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("fehler beim Lesen der Status-Response: %v", err) } if resp.StatusCode != 200 { time.Sleep(pollInterval) continue } // Parse Response var challengeResponse map[string]interface{} if err := json.Unmarshal(body, &challengeResponse); err != nil { time.Sleep(pollInterval) continue } status, ok := challengeResponse["status"].(string) if !ok { time.Sleep(pollInterval) continue } if status == "valid" { return nil } if status == "invalid" { errorDetail := "" errorType := "" if errors, ok := challengeResponse["error"].(map[string]interface{}); ok { if detail, ok := errors["detail"].(string); ok { errorDetail = detail } if errType, ok := errors["type"].(string); ok { errorType = errType } } // Cleanup: Entferne Token if cleanupTokenFunc != nil { cleanupTokenFunc() } errorMsg := fmt.Sprintf("Challenge ist ungültig") if errorType != "" { errorMsg += fmt.Sprintf(" (Typ: %s)", errorType) } if errorDetail != "" { errorMsg += fmt.Sprintf(" - %s", errorDetail) } errorMsg += ". Bitte prüfen Sie die DNS-Einstellungen und versuchen Sie es erneut." return fmt.Errorf(errorMsg) } // Status ist noch "pending", warte und versuche es erneut if attempt < maxAttempts { time.Sleep(pollInterval) } } return fmt.Errorf("challenge-Validierung hat das Zeitlimit überschritten (Status noch nicht 'valid')") } // waitForOrderReady wartet, bis die Order den Status "ready" hat (nach Challenge-Validierung) func waitForOrderReady(ctx *ACMEClientContext, keyPair *ACMEKeyPair, keyID string, orderURL string, traceID, fqdnID string, statusCallback func(status string)) error { maxAttempts := 30 // Maximal 30 Versuche pollInterval := 2 * time.Second // Alle 2 Sekunden prüfen maxConsecutiveErrors := 3 // Maximal 3 aufeinanderfolgende Fehler consecutiveErrors := 0 // Zähler für aufeinanderfolgende Fehler for attempt := 1; attempt <= maxAttempts; attempt++ { statusCallback(fmt.Sprintf("Prüfe Order-Status (%d/%d)...", attempt, maxAttempts)) // Hole Nonce nonce, err := getNonce(ctx.DirectoryURL) if err != nil { log.Printf("[ACME] Fehler beim Abrufen des Nonce: %v", err) consecutiveErrors++ if consecutiveErrors >= maxConsecutiveErrors { return fmt.Errorf("zu viele aufeinanderfolgende Fehler beim Abrufen des Nonce: %v", err) } time.Sleep(pollInterval) continue } // Erstelle JWS für Order-Abfrage (POST-as-GET: leerer Payload) jws, err := createJWSForGet(keyPair, orderURL, keyID, nonce) if err != nil { log.Printf("[ACME] Fehler beim Erstellen des JWS: %v", err) consecutiveErrors++ if consecutiveErrors >= maxConsecutiveErrors { return fmt.Errorf("zu viele aufeinanderfolgende Fehler beim Erstellen des JWS: %v", err) } time.Sleep(pollInterval) continue } // Sende POST-as-GET Request für Order-Status req, err := http.NewRequest("POST", orderURL, bytes.NewBufferString(jws)) if err != nil { log.Printf("[ACME] Fehler beim Erstellen des Requests: %v", err) consecutiveErrors++ if consecutiveErrors >= maxConsecutiveErrors { return fmt.Errorf("zu viele aufeinanderfolgende Fehler beim Erstellen des Requests: %v", err) } time.Sleep(pollInterval) continue } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Do(req) if err != nil { log.Printf("[ACME] Fehler beim Senden des Requests: %v", err) consecutiveErrors++ if consecutiveErrors >= maxConsecutiveErrors { return fmt.Errorf("zu viele aufeinanderfolgende Fehler beim Senden des Requests: %v", err) } time.Sleep(pollInterval) continue } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Printf("[ACME] Fehler beim Lesen der Response: %v", err) consecutiveErrors++ if consecutiveErrors >= maxConsecutiveErrors { return fmt.Errorf("zu viele aufeinanderfolgende Fehler beim Lesen der Response: %v", err) } time.Sleep(pollInterval) continue } if resp.StatusCode != 200 { log.Printf("[ACME] Fehler beim Abrufen des Order-Status (Status %d): %s", resp.StatusCode, string(body)) consecutiveErrors++ if consecutiveErrors >= maxConsecutiveErrors { return fmt.Errorf("zu viele aufeinanderfolgende Fehler beim Abrufen des Order-Status (Status %d): %s", resp.StatusCode, string(body)) } time.Sleep(pollInterval) continue } // Parse Order Response var orderStatusResponse map[string]interface{} if err := json.Unmarshal(body, &orderStatusResponse); err != nil { log.Printf("[ACME] Fehler beim Parsen der Order-Response: %v", err) consecutiveErrors++ if consecutiveErrors >= maxConsecutiveErrors { return fmt.Errorf("zu viele aufeinanderfolgende Fehler beim Parsen der Order-Response: %v", err) } time.Sleep(pollInterval) continue } // Reset error counter bei erfolgreichem Request consecutiveErrors = 0 // Prüfe Order-Status status, ok := orderStatusResponse["status"].(string) if !ok { time.Sleep(pollInterval) continue } if status == "ready" { statusCallback("Order ist bereit für Finalisierung") return nil } if status == "invalid" { errorDetail := "" if errors, ok := orderStatusResponse["error"].(map[string]interface{}); ok { if detail, ok := errors["detail"].(string); ok { errorDetail = detail } } return fmt.Errorf("order ist ungültig: %s", errorDetail) } if status == "valid" { // Order ist bereits valid (Zertifikat wurde bereits ausgestellt) statusCallback("Order ist bereits valid") return nil } // Status ist noch "pending" oder "processing" - weiter warten if attempt < maxAttempts { time.Sleep(pollInterval) } } return fmt.Errorf("order hat das Zeitlimit überschritten (Status noch nicht 'ready' oder 'valid')") } // finalizeOrderAndGetCertificate finalisiert die Order und holt das Zertifikat func finalizeOrderAndGetCertificate(ctx *ACMEClientContext, keyPair *ACMEKeyPair, keyID string, orderURL string, orderResponse map[string]interface{}, traceID, fqdnID string, statusCallback func(status string)) (string, string, error) { // Warte, bis die Order bereit ist (Status "ready" oder "valid") statusCallback("Warte auf Order-Bereitschaft...") if err := waitForOrderReady(ctx, keyPair, keyID, orderURL, traceID, fqdnID, statusCallback); err != nil { return "", "", fmt.Errorf("fehler beim Warten auf Order-Bereitschaft: %v", err) } // Extrahiere finalize URL finalizeURL, ok := orderResponse["finalize"].(string) if !ok || finalizeURL == "" { return "", "", fmt.Errorf("keine finalize URL in Order-Response gefunden") } // Debug: Curl-Befehl für manuelle Abfrage // Extrahiere Domain aus identifiers var domain string if identifiers, ok := orderResponse["identifiers"].([]interface{}); ok && len(identifiers) > 0 { if identifier, ok := identifiers[0].(map[string]interface{}); ok { if value, ok := identifier["value"].(string); ok { domain = value } } } if domain == "" { return "", "", fmt.Errorf("keine Domain in Order-Response gefunden") } // Erstelle separates Key-Pair für das Zertifikat (muss unterschiedlich vom Account-Key sein) certPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return "", "", fmt.Errorf("fehler beim Generieren des Zertifikat-Keys: %v", err) } // Erstelle CSR für die Domain mit dem separaten Key csrDER, err := createCSR(certPrivateKey, domain) if err != nil { return "", "", fmt.Errorf("fehler beim Erstellen des CSR: %v", err) } // Encode CSR als Base64URL csrB64 := base64.RawURLEncoding.EncodeToString(csrDER) // Erstelle Payload für Finalisierung payload := map[string]interface{}{ "csr": csrB64, } // Hole Nonce nonce, err := getNonce(ctx.DirectoryURL) if err != nil { return "", "", fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } // Erstelle JWS jws, err := createJWS(keyPair, payload, finalizeURL, keyID, nonce) if err != nil { return "", "", fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } // Sende Finalize Request req, err := http.NewRequest("POST", finalizeURL, bytes.NewBufferString(jws)) if err != nil { return "", "", fmt.Errorf("fehler beim Erstellen des Finalize-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(req) if err != nil { return "", "", fmt.Errorf("fehler beim Senden des Finalize-Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", "", fmt.Errorf("fehler beim Lesen der Finalize-Response: %v", err) } if resp.StatusCode != 200 { return "", "", fmt.Errorf("fehler bei der Finalisierung (Status %d): %s", resp.StatusCode, string(body)) } // Parse Finalize Response var finalizeResponse map[string]interface{} if err := json.Unmarshal(body, &finalizeResponse); err != nil { return "", "", fmt.Errorf("fehler beim Parsen der Finalize-Response: %v", err) } // Warte auf Zertifikat (Polling) statusCallback("Warte auf Zertifikat...") certURL, ok := finalizeResponse["certificate"].(string) if !ok || certURL == "" { // Polling: Prüfe Order-Status bis certificate URL verfügbar ist maxAttempts := 30 pollInterval := 2 * time.Second for attempt := 1; attempt <= maxAttempts; attempt++ { statusCallback(fmt.Sprintf("Prüfe Order-Status (%d/%d)...", attempt, maxAttempts)) nonce, err := getNonce(ctx.DirectoryURL) if err != nil { return "", "", fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } jws, err := createJWSForGet(keyPair, orderURL, keyID, nonce) if err != nil { return "", "", fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } req, err := http.NewRequest("POST", orderURL, bytes.NewBufferString(jws)) if err != nil { return "", "", fmt.Errorf("fehler beim Erstellen des Order-Status-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return "", "", fmt.Errorf("fehler beim Senden des Order-Status-Requests: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", "", fmt.Errorf("fehler beim Lesen der Order-Status-Response: %v", err) } if resp.StatusCode != 200 { time.Sleep(pollInterval) continue } var orderStatusResponse map[string]interface{} if err := json.Unmarshal(body, &orderStatusResponse); err != nil { time.Sleep(pollInterval) continue } if cert, ok := orderStatusResponse["certificate"].(string); ok && cert != "" { certURL = cert break } if attempt < maxAttempts { time.Sleep(pollInterval) } } } if certURL == "" { return "", "", fmt.Errorf("keine Zertifikat-URL in Order-Response gefunden") } statusCallback("Hole Zertifikat...") // Hole Zertifikat nonce, err = getNonce(ctx.DirectoryURL) if err != nil { return "", "", fmt.Errorf("fehler beim Abrufen des Nonce: %v", err) } jws, err = createJWSForGet(keyPair, certURL, keyID, nonce) if err != nil { return "", "", fmt.Errorf("fehler beim Erstellen des JWS: %v", err) } req, err = http.NewRequest("POST", certURL, bytes.NewBufferString(jws)) if err != nil { return "", "", fmt.Errorf("fehler beim Erstellen des Certificate-Requests: %v", err) } req.Header.Set("Content-Type", "application/jose+json") req.Header.Set("Accept", "application/pem-certificate-chain") resp, err = client.Do(req) if err != nil { return "", "", fmt.Errorf("fehler beim Senden des Certificate-Requests: %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) return "", "", fmt.Errorf("fehler beim Abrufen des Zertifikats (Status %d): %s", resp.StatusCode, string(body)) } certPEM, err := io.ReadAll(resp.Body) if err != nil { return "", "", fmt.Errorf("fehler beim Lesen des Zertifikats: %v", err) } // Konvertiere Private Key zu PEM (verwende den separaten Zertifikat-Key, nicht den Account-Key) keyPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(certPrivateKey), }) return string(certPEM), string(keyPEM), nil } // createCSR erstellt einen Certificate Signing Request func createCSR(privateKey *rsa.PrivateKey, domain string) ([]byte, error) { template := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: domain, }, DNSNames: []string{domain}, } csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey) if err != nil { return nil, fmt.Errorf("fehler beim Erstellen des CSR: %v", err) } return csrDER, nil }