Files
certigo-dummy-ca/main.go
2025-11-20 13:30:08 +01:00

430 lines
12 KiB
Go

package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"log"
"math/big"
"net/http"
"os"
"sync"
"time"
"github.com/gorilla/mux"
)
// CertificateStore speichert signierte Zertifikate
type CertificateStore struct {
mu sync.RWMutex
certificates map[string]*StoredCertificate
}
// StoredCertificate enthält das signierte Zertifikat und Metadaten
type StoredCertificate struct {
ID string
Certificate *x509.Certificate
PEM string
CreatedAt time.Time
}
// CA repräsentiert die Certificate Authority
type CA struct {
rootCert *x509.Certificate
rootKey *rsa.PrivateKey
certStore *CertificateStore
serialNumber *big.Int
mu sync.Mutex
}
// CSRRequest repräsentiert eine CSR-Anfrage
type CSRRequest struct {
CSR string `json:"csr"` // Base64-kodierter PEM-formatierter CSR
Action string `json:"action"` // z.B. "sign"
ValidityDays int `json:"validity_days"` // Gültigkeitsdauer in Tagen (optional, default: 365)
}
// CSRResponse repräsentiert die Antwort auf eine CSR-Anfrage
type CSRResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
Certificate string `json:"certificate,omitempty"` // PEM-formatierter Zertifikat
}
// CertificateResponse repräsentiert die Antwort beim Abruf eines Zertifikats
type CertificateResponse struct {
ID string `json:"id"`
Certificate string `json:"certificate"`
CreatedAt time.Time `json:"created_at"`
}
// NewCA erstellt eine neue Certificate Authority
func NewCA() (*CA, error) {
// Root-Zertifikat und Key generieren
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("fehler beim Generieren des Root-Keys: %v", err)
}
// Root-Zertifikat erstellen
rootTemplate := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Dummy CA"},
Country: []string{"DE"},
Province: []string{""},
Locality: []string{""},
StreetAddress: []string{""},
PostalCode: []string{""},
CommonName: "Dummy CA Root Certificate",
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0), // 10 Jahre gültig
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 2,
MaxPathLenZero: false,
}
rootCertDER, err := x509.CreateCertificate(rand.Reader, rootTemplate, rootTemplate, &rootKey.PublicKey, rootKey)
if err != nil {
return nil, fmt.Errorf("fehler beim Erstellen des Root-Zertifikats: %v", err)
}
rootCert, err := x509.ParseCertificate(rootCertDER)
if err != nil {
return nil, fmt.Errorf("fehler beim Parsen des Root-Zertifikats: %v", err)
}
return &CA{
rootCert: rootCert,
rootKey: rootKey,
certStore: &CertificateStore{certificates: make(map[string]*StoredCertificate)},
serialNumber: big.NewInt(2),
}, nil
}
// SignCSR signiert einen Certificate Signing Request
func (ca *CA) SignCSR(csrPEM string, validityDays int) (string, error) {
// PEM-Block dekodieren
block, _ := pem.Decode([]byte(csrPEM))
if block == nil {
return "", fmt.Errorf("ungültiger PEM-Block")
}
// CSR parsen
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return "", fmt.Errorf("fehler beim Parsen des CSR: %v", err)
}
// CSR validieren
if err := csr.CheckSignature(); err != nil {
return "", fmt.Errorf("ungültige CSR-Signatur: %v", err)
}
// Serialnummer inkrementieren
ca.mu.Lock()
serial := new(big.Int).Set(ca.serialNumber)
ca.serialNumber.Add(ca.serialNumber, big.NewInt(1))
ca.mu.Unlock()
// Zertifikat-Template erstellen
template := &x509.Certificate{
SerialNumber: serial,
Subject: csr.Subject,
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(0, 0, validityDays),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
}
// SANs (Subject Alternative Names) vom CSR kopieren
if len(csr.DNSNames) > 0 {
template.DNSNames = csr.DNSNames
}
if len(csr.IPAddresses) > 0 {
template.IPAddresses = csr.IPAddresses
}
if len(csr.EmailAddresses) > 0 {
template.EmailAddresses = csr.EmailAddresses
}
// Zertifikat signieren
certDER, err := x509.CreateCertificate(rand.Reader, template, ca.rootCert, csr.PublicKey, ca.rootKey)
if err != nil {
return "", fmt.Errorf("fehler beim Signieren des Zertifikats: %v", err)
}
// Zertifikat parsen
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return "", fmt.Errorf("fehler beim Parsen des signierten Zertifikats: %v", err)
}
// PEM-formatieren
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
// In Store speichern
certID := fmt.Sprintf("%x", cert.SerialNumber.Bytes())
ca.certStore.mu.Lock()
ca.certStore.certificates[certID] = &StoredCertificate{
ID: certID,
Certificate: cert,
PEM: string(certPEM),
CreatedAt: time.Now(),
}
ca.certStore.mu.Unlock()
return certID, nil
}
// GetCertificate ruft ein Zertifikat anhand der ID ab
func (ca *CA) GetCertificate(id string) (*StoredCertificate, error) {
ca.certStore.mu.RLock()
defer ca.certStore.mu.RUnlock()
cert, exists := ca.certStore.certificates[id]
if !exists {
return nil, fmt.Errorf("zertifikat mit ID %s nicht gefunden", id)
}
return cert, nil
}
// GetRootCertificate gibt das Root-Zertifikat zurück
func (ca *CA) GetRootCertificate() string {
rootCertPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ca.rootCert.Raw,
})
return string(rootCertPEM)
}
// handleSubmitCSR behandelt POST-Anfragen zum Einreichen eines CSR
func (ca *CA) handleSubmitCSR(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "nur POST erlaubt", http.StatusMethodNotAllowed)
return
}
var req CSRRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, fmt.Sprintf("fehler beim Dekodieren der Anfrage: %v", err), http.StatusBadRequest)
return
}
// CSR dekodieren (Base64)
csrBytes, err := base64.StdEncoding.DecodeString(req.CSR)
if err != nil {
http.Error(w, fmt.Sprintf("fehler beim Dekodieren des CSR: %v", err), http.StatusBadRequest)
return
}
csrPEM := string(csrBytes)
// Validity Days standardmäßig auf 365 setzen
validityDays := req.ValidityDays
if validityDays <= 0 {
validityDays = 365
}
// Action prüfen
if req.Action != "sign" {
http.Error(w, "ungültige Action. Erlaubt: 'sign'", http.StatusBadRequest)
return
}
// CSR signieren
certID, err := ca.SignCSR(csrPEM, validityDays)
if err != nil {
http.Error(w, fmt.Sprintf("fehler beim Signieren: %v", err), http.StatusInternalServerError)
return
}
// Zertifikat abrufen
storedCert, err := ca.GetCertificate(certID)
if err != nil {
http.Error(w, fmt.Sprintf("fehler beim Abrufen des Zertifikats: %v", err), http.StatusInternalServerError)
return
}
// Antwort senden
response := CSRResponse{
ID: certID,
Status: "success",
Message: "CSR erfolgreich signiert",
Certificate: storedCert.PEM,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// handleGetCertificate behandelt GET-Anfragen zum Abruf eines Zertifikats
func (ca *CA) handleGetCertificate(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
certID := vars["id"]
if certID == "" {
http.Error(w, "zertifikat-ID erforderlich", http.StatusBadRequest)
return
}
storedCert, err := ca.GetCertificate(certID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
response := CertificateResponse{
ID: storedCert.ID,
Certificate: storedCert.PEM,
CreatedAt: storedCert.CreatedAt,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// handleGetRootCertificate gibt das Root-Zertifikat zurück
func (ca *CA) handleGetRootCertificate(w http.ResponseWriter, r *http.Request) {
rootCert := ca.GetRootCertificate()
w.Header().Set("Content-Type", "application/x-pem-file")
w.Write([]byte(rootCert))
}
// responseWriter ist ein Wrapper für http.ResponseWriter, um den Status Code zu erfassen
type responseWriter struct {
http.ResponseWriter
statusCode int
bytesWritten int64
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{w, http.StatusOK, 0}
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.bytesWritten += int64(n)
return n, err
}
// loggingMiddleware loggt alle HTTP-Requests mit Details
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// ResponseWriter wrappen, um Status Code zu erfassen
wrapped := newResponseWriter(w)
// Request verarbeiten
next.ServeHTTP(wrapped, r)
// Logging
duration := time.Since(start)
// Farbige Ausgabe für bessere Lesbarkeit
statusColor := ""
statusReset := ""
if wrapped.statusCode >= 200 && wrapped.statusCode < 300 {
statusColor = "\033[32m" // Grün für Erfolg
statusReset = "\033[0m"
} else if wrapped.statusCode >= 400 && wrapped.statusCode < 500 {
statusColor = "\033[33m" // Gelb für Client-Fehler
statusReset = "\033[0m"
} else if wrapped.statusCode >= 500 {
statusColor = "\033[31m" // Rot für Server-Fehler
statusReset = "\033[0m"
}
// Log-Format: [Zeit] METHOD Pfad Status Dauer Größe IP User-Agent
log.Printf("[%s] %s %s %s%d%s %v %d bytes %s %s",
start.Format("2006-01-02 15:04:05"),
r.Method,
r.URL.Path,
statusColor,
wrapped.statusCode,
statusReset,
duration,
wrapped.bytesWritten,
r.RemoteAddr,
r.UserAgent(),
)
// Bei POST-Requests auch Query-Parameter und Content-Length loggen
if r.Method == "POST" {
if r.URL.RawQuery != "" {
log.Printf(" Query: %s", r.URL.RawQuery)
}
if r.ContentLength > 0 {
log.Printf(" Content-Length: %d bytes", r.ContentLength)
}
}
})
}
func main() {
// CA initialisieren
ca, err := NewCA()
if err != nil {
log.Fatalf("fehler beim Initialisieren der CA: %v", err)
}
// Router erstellen
r := mux.NewRouter()
// API-Endpunkte
r.HandleFunc("/csr", ca.handleSubmitCSR).Methods("POST")
r.HandleFunc("/certificate/{id}", ca.handleGetCertificate).Methods("GET")
r.HandleFunc("/root", ca.handleGetRootCertificate).Methods("GET")
// Health-Check
r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}).Methods("GET")
// Logging-Middleware auf alle Routen anwenden
loggedRouter := loggingMiddleware(r)
// Logger konfigurieren für bessere Ausgabe
log.SetOutput(os.Stdout)
log.SetFlags(0) // Keine Standard-Präfixe, wir formatieren selbst
log.Println("========================================")
log.Println("Dummy CA Server startet auf Port 8088")
log.Println("========================================")
log.Println("Endpunkte:")
log.Println(" POST /csr - CSR einreichen und signieren")
log.Println(" GET /certificate/{id} - Zertifikat abrufen")
log.Println(" GET /root - Root-Zertifikat abrufen")
log.Println(" GET /health - Health-Check")
log.Println("")
log.Println("Alle API-Anfragen werden geloggt...")
log.Println("========================================")
log.Println("")
if err := http.ListenAndServe(":8088", loggedRouter); err != nil {
log.Fatalf("fehler beim Starten des Servers: %v", err)
}
}