1449 lines
46 KiB
Go
1449 lines
46 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"
|
|
)
|
|
|
|
// 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
|
|
}
|