Files
certigo/backend/acme_client.go

1412 lines
44 KiB
Go

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"
)
const (
// Let's Encrypt Staging für Tests
acmeStagingDirectory = "https://acme-staging-v02.api.letsencrypt.org/directory"
acmeStagingNewAccount = "https://acme-staging-v02.api.letsencrypt.org/acme/new-acct"
acmeStagingNewOrder = "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"
)
// 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() (string, error) {
// Rufe Directory-Endpoint auf, um einen Nonce zu bekommen
req, err := http.NewRequest("HEAD", acmeStagingDirectory, 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 Let's Encrypt
func createAccount(keyPair *ACMEKeyPair, email string, traceID, fqdnID string, statusCallback func(status string)) (string, error) {
statusCallback("Erstelle Account bei Let's Encrypt...")
// Hole Nonce vom Server
nonce, err := getNonce()
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, acmeStagingNewAccount, "", nonce)
if err != nil {
return "", fmt.Errorf("fehler beim Erstellen des JWS: %v", err)
}
// Sende Request
req, err := http.NewRequest("POST", acmeStagingNewAccount, 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 Let's Encrypt
func createOrder(keyPair *ACMEKeyPair, keyID string, domains []string, traceID, fqdnID string, statusCallback func(status string)) (string, map[string]interface{}, error) {
statusCallback("Erstelle Order bei Let's Encrypt...")
// Hole Nonce vom Server
nonce, err := getNonce()
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, acmeStagingNewOrder, keyID, nonce)
if err != nil {
return "", nil, fmt.Errorf("fehler beim Erstellen des JWS: %v", err)
}
// Sende Request
req, err := http.NewRequest("POST", acmeStagingNewOrder, 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 Let's Encrypt (manuell ohne LEGO)
// 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(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 bei Let's Encrypt (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("Erstelle Account bei Let's Encrypt...")
result.StepStatus["ACCOUNT_ERSTELLUNG"] = "loading"
keyID, err = createAccount(keyPair, 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 bei Let's Encrypt
baseFqdn := fqdn
if strings.HasPrefix(baseFqdn, "*.") {
baseFqdn = baseFqdn[2:]
}
log.Printf("[ACME] Schritt 3: Erstelle Order für Domain: %s", baseFqdn)
statusCallback("Erstelle Order bei Let's Encrypt...")
result.StepStatus["ORDER_ERSTELLUNG"] = "loading"
orderURL, orderResponse, err := createOrder(keyPair, 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(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(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(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(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(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(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()
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(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()
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 bei Let's Encrypt
func activateChallenge(keyPair *ACMEKeyPair, keyID string, challengeURL string, traceID, fqdnID string) error {
// Hole Nonce
nonce, err := getNonce()
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(keyPair *ACMEKeyPair, keyID string, challengeURL string) error {
// Hole Nonce
nonce, err := getNonce()
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(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()
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(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()
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(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(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()
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()
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()
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
}