430 lines
12 KiB
Go
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)
|
|
}
|
|
}
|