first commit
This commit is contained in:
429
main.go
Normal file
429
main.go
Normal file
@@ -0,0 +1,429 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user