commit 6c83b95a7b74c2586fc0488163a365afc31c17ea Author: Nick Adam Date: Thu Nov 20 13:30:08 2025 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..007df52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Binaries +certigo-dummy-ca +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test files +test.key +test.csr +test.crt +root.crt +*.pem +*.key +*.crt +*.csr + +# Go +*.test +*.out +go.work + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + diff --git a/API.md b/API.md new file mode 100644 index 0000000..15f93bb --- /dev/null +++ b/API.md @@ -0,0 +1,386 @@ +# Dummy CA - API Dokumentation + +Diese Dokumentation beschreibt die REST-API für die externe Anbindung an die Dummy CA. + +**Base URL:** `http://localhost:8088` (oder die entsprechende Server-Adresse) + +--- + +## Endpunkte + +### 1. Health Check + +Prüft, ob der Server erreichbar ist. + +**Endpoint:** `GET /health` + +**Response:** +```json +{ + "status": "ok" +} +``` + +**Status Codes:** +- `200 OK` - Server ist erreichbar + +--- + +### 2. CSR einreichen und signieren + +Reicht einen Certificate Signing Request (CSR) ein und lässt ihn signieren. + +**Endpoint:** `POST /csr` + +**Content-Type:** `application/json` + +**Request Body:** +```json +{ + "csr": "BASE64_ENCODED_CSR_PEM", + "action": "sign", + "validity_days": 365 +} +``` + +**Parameter:** +- `csr` (string, erforderlich): Der CSR im PEM-Format, Base64-kodiert +- `action` (string, erforderlich): Aktuell nur `"sign"` erlaubt +- `validity_days` (integer, optional): Gültigkeitsdauer in Tagen (Standard: 365) + +**Response (Erfolg):** +```json +{ + "id": "0202", + "status": "success", + "message": "CSR erfolgreich signiert", + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIRAK...\n-----END CERTIFICATE-----\n" +} +``` + +**Response (Fehler):** +```json +{ + "error": "fehler beim Dekodieren des CSR: illegal base64 data" +} +``` + +**Status Codes:** +- `200 OK` - CSR erfolgreich signiert +- `400 Bad Request` - Ungültige Anfrage (fehlende Parameter, ungültiger CSR, etc.) +- `405 Method Not Allowed` - Falsche HTTP-Methode +- `500 Internal Server Error` - Server-Fehler beim Signieren + +--- + +### 3. Zertifikat abrufen + +Ruft ein signiertes Zertifikat anhand der Zertifikat-ID ab. + +**Endpoint:** `GET /certificate/{id}` + +**URL Parameter:** +- `id` (string, erforderlich): Die Zertifikat-ID (aus der CSR-Response) + +**Response (Erfolg):** +```json +{ + "id": "0202", + "certificate": "-----BEGIN CERTIFICATE-----\nMIIDXTCCAkWgAwIBAgIRAK...\n-----END CERTIFICATE-----\n", + "created_at": "2024-01-15T10:30:00Z" +} +``` + +**Response (Fehler):** +``` +zertifikat mit ID 0202 nicht gefunden +``` + +**Status Codes:** +- `200 OK` - Zertifikat gefunden +- `400 Bad Request` - Fehlende Zertifikat-ID +- `404 Not Found` - Zertifikat nicht gefunden + +--- + +### 4. Root-Zertifikat abrufen + +Ruft das Root-Zertifikat der CA ab. + +**Endpoint:** `GET /root` + +**Response:** +``` +-----BEGIN CERTIFICATE----- +MIIDXTCCAkWgAwIBAgIRAK... +-----END CERTIFICATE----- +``` + +**Content-Type:** `application/x-pem-file` + +**Status Codes:** +- `200 OK` - Root-Zertifikat erfolgreich abgerufen + +--- + +## Beispiel-Implementierungen + +### Python + +```python +import requests +import base64 +import json + +CA_URL = "http://localhost:8088" + +# 1. CSR aus Datei lesen und Base64 kodieren +with open("request.csr", "rb") as f: + csr_pem = f.read() + csr_b64 = base64.b64encode(csr_pem).decode('utf-8') + +# 2. CSR einreichen +response = requests.post( + f"{CA_URL}/csr", + json={ + "csr": csr_b64, + "action": "sign", + "validity_days": 365 + }, + headers={"Content-Type": "application/json"} +) + +if response.status_code == 200: + data = response.json() + cert_id = data["id"] + certificate = data["certificate"] + + # Zertifikat speichern + with open("signed.crt", "w") as f: + f.write(certificate) + + print(f"Zertifikat-ID: {cert_id}") +else: + print(f"Fehler: {response.status_code} - {response.text}") + +# 3. Zertifikat später abrufen +cert_response = requests.get(f"{CA_URL}/certificate/{cert_id}") +if cert_response.status_code == 200: + cert_data = cert_response.json() + print(f"Zertifikat erstellt am: {cert_data['created_at']}") +``` + +### cURL + +```bash +# CSR einreichen +CSR_B64=$(cat request.csr | base64 -w 0) + +curl -X POST http://localhost:8088/csr \ + -H "Content-Type: application/json" \ + -d "{ + \"csr\": \"$CSR_B64\", + \"action\": \"sign\", + \"validity_days\": 365 + }" + +# Zertifikat abrufen +curl http://localhost:8088/certificate/0202 + +# Root-Zertifikat abrufen +curl http://localhost:8088/root > root.crt +``` + +### JavaScript/Node.js + +```javascript +const axios = require('axios'); +const fs = require('fs'); + +const CA_URL = 'http://localhost:8088'; + +async function submitCSR(csrPath) { + // CSR lesen und Base64 kodieren + const csrPEM = fs.readFileSync(csrPath, 'utf8'); + const csrB64 = Buffer.from(csrPEM).toString('base64'); + + try { + // CSR einreichen + const response = await axios.post(`${CA_URL}/csr`, { + csr: csrB64, + action: 'sign', + validity_days: 365 + }); + + const { id, certificate } = response.data; + + // Zertifikat speichern + fs.writeFileSync('signed.crt', certificate); + + console.log(`Zertifikat-ID: ${id}`); + return id; + } catch (error) { + console.error('Fehler:', error.response?.data || error.message); + throw error; + } +} + +async function getCertificate(certId) { + try { + const response = await axios.get(`${CA_URL}/certificate/${certId}`); + return response.data; + } catch (error) { + console.error('Fehler:', error.response?.data || error.message); + throw error; + } +} + +// Verwendung +submitCSR('request.csr') + .then(certId => getCertificate(certId)) + .then(data => console.log('Zertifikat erstellt:', data.created_at)) + .catch(console.error); +``` + +### Go + +```go +package main + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +const CA_URL = "http://localhost:8088" + +type CSRRequest struct { + CSR string `json:"csr"` + Action string `json:"action"` + ValidityDays int `json:"validity_days"` +} + +type CSRResponse struct { + ID string `json:"id"` + Status string `json:"status"` + Message string `json:"message"` + Certificate string `json:"certificate"` +} + +func submitCSR(csrPath string) (string, error) { + // CSR lesen + csrPEM, err := ioutil.ReadFile(csrPath) + if err != nil { + return "", err + } + + // Base64 kodieren + csrB64 := base64.StdEncoding.EncodeToString(csrPEM) + + // Request erstellen + reqBody := CSRRequest{ + CSR: csrB64, + Action: "sign", + ValidityDays: 365, + } + + jsonData, _ := json.Marshal(reqBody) + + // HTTP Request + resp, err := http.Post(CA_URL+"/csr", "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("fehler: %s", string(body)) + } + + var csrResp CSRResponse + json.Unmarshal(body, &csrResp) + + // Zertifikat speichern + ioutil.WriteFile("signed.crt", []byte(csrResp.Certificate), 0644) + + return csrResp.ID, nil +} +``` + +--- + +## CSR-Format + +Der CSR muss im PEM-Format vorliegen und Base64-kodiert übertragen werden. + +**Beispiel CSR (PEM):** +``` +-----BEGIN CERTIFICATE REQUEST----- +MIICVjCCAT4CAQAwEjEQMA4GA1UEAwwHZXhhbXBsZTEwggEiMA0GCSqGSIb3DQEB +... +-----END CERTIFICATE REQUEST----- +``` + +**Erstellung eines CSR:** +```bash +# Private Key generieren +openssl genrsa -out private.key 2048 + +# CSR erstellen +openssl req -new -key private.key -out request.csr \ + -subj "/CN=example.com/O=Example Org/C=DE" +``` + +--- + +## Fehlerbehandlung + +Alle Fehler werden als HTTP-Status-Codes zurückgegeben: + +- **400 Bad Request**: Ungültige Anfrage (fehlende/ungültige Parameter) +- **404 Not Found**: Ressource nicht gefunden (z.B. Zertifikat-ID existiert nicht) +- **405 Method Not Allowed**: Falsche HTTP-Methode verwendet +- **500 Internal Server Error**: Server-seitiger Fehler + +Fehlermeldungen werden im Response-Body als Text zurückgegeben. + +--- + +## Wichtige Hinweise + +1. **Zertifikat-Speicher**: Zertifikate werden nur im Speicher gehalten und gehen nach einem Server-Neustart verloren. + +2. **Keine Authentifizierung**: Die API hat aktuell keine Authentifizierung. Für Produktionsumgebungen sollte dies hinzugefügt werden. + +3. **CSR-Validierung**: Die CA validiert die CSR-Signatur, aber nicht den Inhalt des CSR. + +4. **Serialnummern**: Jedes Zertifikat erhält eine eindeutige, automatisch inkrementierte Serialnummer. + +5. **Gültigkeitsdauer**: Standardmäßig 365 Tage, kann über `validity_days` angepasst werden. + +--- + +## Testen der API + +Sie können die API mit dem bereitgestellten Beispiel-Skript testen: + +```bash +./example.sh +``` + +Oder manuell mit cURL: + +```bash +# Health Check +curl http://localhost:8088/health + +# Root-Zertifikat +curl http://localhost:8088/root +``` + diff --git a/README.md b/README.md new file mode 100644 index 0000000..68701d2 --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# Dummy CA - Certificate Authority in Go + +Eine einfache Certificate Authority (CA) in Go, die über eine REST-API Certificate Signing Requests (CSR) entgegennimmt, signiert und Zertifikate bereitstellt. + +## Features + +- Automatische Generierung eines Root-Zertifikats beim Start +- REST-API zum Einreichen und Signieren von CSRs +- Abruf von signierten Zertifikaten über GET-Request +- Abruf des Root-Zertifikats + +## Installation + +```bash +go mod download +``` + +## Starten + +```bash +go run main.go +``` + +Der Server läuft standardmäßig auf Port 8088. + +## API-Endpunkte + +### POST /csr +Reicht einen CSR ein und lässt ihn signieren. + +**Request Body:** +```json +{ + "csr": "BASE64_ENCODED_CSR_PEM", + "action": "sign", + "validity_days": 365 +} +``` + +**Response:** +```json +{ + "id": "zertifikat-id", + "status": "success", + "message": "CSR erfolgreich signiert", + "certificate": "-----BEGIN CERTIFICATE-----\n..." +} +``` + +### GET /certificate/{id} +Ruft ein signiertes Zertifikat anhand der ID ab. + +**Response:** +```json +{ + "id": "zertifikat-id", + "certificate": "-----BEGIN CERTIFICATE-----\n...", + "created_at": "2024-01-01T12:00:00Z" +} +``` + +### GET /root +Ruft das Root-Zertifikat der CA ab (PEM-Format). + +### GET /health +Health-Check-Endpunkt. + +## Beispiel: CSR erstellen und einreichen + +### 1. CSR erstellen + +```bash +# Private Key generieren +openssl genrsa -out private.key 2048 + +# CSR erstellen +openssl req -new -key private.key -out request.csr -subj "/CN=example.com" +``` + +### 2. CSR in Base64 kodieren + +```bash +# CSR in Base64 kodieren +CSR_B64=$(cat request.csr | base64 -w 0) +``` + +### 3. CSR an die CA senden + +```bash +curl -X POST http://localhost:8088/csr \ + -H "Content-Type: application/json" \ + -d "{ + \"csr\": \"$CSR_B64\", + \"action\": \"sign\", + \"validity_days\": 365 + }" +``` + +### 4. Zertifikat abrufen + +```bash +# Mit der ID aus der vorherigen Antwort +curl http://localhost:8088/certificate/{id} +``` + +### 5. Root-Zertifikat abrufen + +```bash +curl http://localhost:8088/root > root.crt +``` + +## Beispiel-Skript + +Ein Beispiel-Skript zum Testen der API: + +```bash +#!/bin/bash + +# 1. Private Key und CSR erstellen +openssl genrsa -out test.key 2048 +openssl req -new -key test.key -out test.csr -subj "/CN=test.example.com" + +# 2. CSR kodieren und einreichen +CSR_B64=$(cat test.csr | base64 -w 0) +RESPONSE=$(curl -s -X POST http://localhost:8088/csr \ + -H "Content-Type: application/json" \ + -d "{ + \"csr\": \"$CSR_B64\", + \"action\": \"sign\", + \"validity_days\": 365 + }") + +# 3. Zertifikat-ID extrahieren +CERT_ID=$(echo $RESPONSE | jq -r '.id') +echo "Zertifikat-ID: $CERT_ID" + +# 4. Zertifikat abrufen +curl -s http://localhost:8088/certificate/$CERT_ID | jq -r '.certificate' > test.crt + +# 5. Zertifikat verifizieren +openssl x509 -in test.crt -text -noout +``` + +## Technische Details + +- Root-Zertifikat: 2048-bit RSA, 10 Jahre Gültigkeit +- Signierte Zertifikate: Standardmäßig 365 Tage Gültigkeit (konfigurierbar) +- Zertifikat-Speicher: In-Memory (verloren nach Neustart) +- Serialnummern: Automatisch inkrementiert + +## Sicherheitshinweise + +⚠️ **Diese CA ist nur für Test- und Entwicklungszwecke gedacht!** + +- Keine Persistierung der Zertifikate +- Keine Authentifizierung der API +- Keine Validierung der CSR-Inhalte +- Nicht für Produktionsumgebungen geeignet + diff --git a/api_example.sh b/api_example.sh new file mode 100755 index 0000000..395566c --- /dev/null +++ b/api_example.sh @@ -0,0 +1,162 @@ +#!/bin/bash + +# Beispiel-Skript für externe Systeme zur Anbindung an die Dummy CA +# Dieses Skript zeigt, wie man die API von einem externen System aus nutzt + +set -e + +CA_URL="${CA_URL:-http://localhost:8088}" + +# Farben für Output +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${YELLOW}=== Dummy CA API - Externes System Beispiel ===${NC}" +echo "" + +# Funktion: Health Check +check_health() { + echo "1. Prüfe Server-Verfügbarkeit..." + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$CA_URL/health") + + if [ "$HTTP_CODE" = "200" ]; then + echo -e "${GREEN}✓ Server ist erreichbar${NC}" + return 0 + else + echo -e "${RED}✗ Server nicht erreichbar (HTTP $HTTP_CODE)${NC}" + return 1 + fi +} + +# Funktion: CSR einreichen +submit_csr() { + local csr_file="$1" + local validity_days="${2:-365}" + + if [ ! -f "$csr_file" ]; then + echo -e "${RED}✗ CSR-Datei nicht gefunden: $csr_file${NC}" + return 1 + fi + + echo "2. Reiche CSR ein..." + + # CSR Base64 kodieren + CSR_B64=$(cat "$csr_file" | base64 -w 0) + + # CSR einreichen + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$CA_URL/csr" \ + -H "Content-Type: application/json" \ + -d "{ + \"csr\": \"$CSR_B64\", + \"action\": \"sign\", + \"validity_days\": $validity_days + }") + + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" = "200" ]; then + CERT_ID=$(echo "$BODY" | jq -r '.id') + CERT_PEM=$(echo "$BODY" | jq -r '.certificate') + + # Zertifikat speichern + echo "$CERT_PEM" > "certificate_${CERT_ID}.crt" + + echo -e "${GREEN}✓ CSR erfolgreich signiert${NC}" + echo " Zertifikat-ID: $CERT_ID" + echo " Zertifikat gespeichert in: certificate_${CERT_ID}.crt" + echo "$CERT_ID" + return 0 + else + echo -e "${RED}✗ Fehler beim Signieren (HTTP $HTTP_CODE)${NC}" + echo "$BODY" | jq . 2>/dev/null || echo "$BODY" + return 1 + fi +} + +# Funktion: Zertifikat abrufen +get_certificate() { + local cert_id="$1" + + if [ -z "$cert_id" ]; then + echo -e "${RED}✗ Zertifikat-ID erforderlich${NC}" + return 1 + fi + + echo "3. Rufe Zertifikat ab (ID: $cert_id)..." + + RESPONSE=$(curl -s -w "\n%{http_code}" "$CA_URL/certificate/$cert_id") + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | sed '$d') + + if [ "$HTTP_CODE" = "200" ]; then + CERT_PEM=$(echo "$BODY" | jq -r '.certificate') + CREATED_AT=$(echo "$BODY" | jq -r '.created_at') + + echo -e "${GREEN}✓ Zertifikat abgerufen${NC}" + echo " Erstellt am: $CREATED_AT" + echo "$CERT_PEM" + return 0 + else + echo -e "${RED}✗ Zertifikat nicht gefunden (HTTP $HTTP_CODE)${NC}" + echo "$BODY" + return 1 + fi +} + +# Funktion: Root-Zertifikat abrufen +get_root_certificate() { + echo "4. Rufe Root-Zertifikat ab..." + + ROOT_CERT=$(curl -s "$CA_URL/root") + + if [ -n "$ROOT_CERT" ]; then + echo "$ROOT_CERT" > "root_ca.crt" + echo -e "${GREEN}✓ Root-Zertifikat gespeichert in: root_ca.crt${NC}" + return 0 + else + echo -e "${RED}✗ Fehler beim Abrufen des Root-Zertifikats${NC}" + return 1 + fi +} + +# Hauptfunktion +main() { + # Health Check + if ! check_health; then + echo "" + echo "Bitte starten Sie den Server mit: go run main.go" + exit 1 + fi + echo "" + + # Wenn CSR-Datei als Argument übergeben wurde + if [ -n "$1" ]; then + CERT_ID=$(submit_csr "$1" "${2:-365}") + echo "" + + if [ -n "$CERT_ID" ]; then + get_certificate "$CERT_ID" > /dev/null + echo "" + fi + else + echo "Verwendung:" + echo " $0 [validity_days]" + echo "" + echo "Beispiel:" + echo " $0 request.csr 365" + echo "" + fi + + # Root-Zertifikat abrufen + get_root_certificate + echo "" + + echo -e "${YELLOW}=== Fertig ===${NC}" +} + +# Skript ausführen +main "$@" + diff --git a/example.sh b/example.sh new file mode 100755 index 0000000..fd44efe --- /dev/null +++ b/example.sh @@ -0,0 +1,88 @@ +#!/bin/bash + +# Beispiel-Skript zum Testen der Dummy CA API + +set -e + +CA_URL="http://localhost:8088" + +echo "=== Dummy CA Test ===" +echo "" + +# 1. Health-Check +echo "1. Health-Check..." +HEALTH_RESPONSE=$(curl -s -w "\n%{http_code}" "$CA_URL/health") +HTTP_CODE=$(echo "$HEALTH_RESPONSE" | tail -n1) +BODY=$(echo "$HEALTH_RESPONSE" | sed '$d') + +if [ "$HTTP_CODE" = "200" ]; then + echo "$BODY" | jq . 2>/dev/null || echo "$BODY" + echo "✓ Server ist erreichbar" +else + echo "✗ Server nicht erreichbar (HTTP $HTTP_CODE)" + echo " Stelle sicher, dass der Server auf Port 8088 läuft:" + echo " go run main.go" + exit 1 +fi +echo "" + +# 2. Private Key und CSR erstellen +echo "2. Erstelle Private Key und CSR..." +openssl genrsa -out test.key 2048 2>/dev/null +openssl req -new -key test.key -out test.csr -subj "/CN=test.example.com/O=Test Org" 2>/dev/null +echo "✓ Private Key und CSR erstellt" +echo "" + +# 3. CSR kodieren und einreichen +echo "3. Reiche CSR ein..." +CSR_B64=$(cat test.csr | base64 -w 0) +RESPONSE=$(curl -s -X POST "$CA_URL/csr" \ + -H "Content-Type: application/json" \ + -d "{ + \"csr\": \"$CSR_B64\", + \"action\": \"sign\", + \"validity_days\": 365 + }") + +CERT_ID=$(echo $RESPONSE | jq -r '.id' 2>/dev/null) +if [ -z "$CERT_ID" ] || [ "$CERT_ID" = "null" ]; then + echo "✗ Fehler beim Signieren des CSR:" + echo "$RESPONSE" | jq . 2>/dev/null || echo "$RESPONSE" + exit 1 +fi +echo "✓ CSR signiert - Zertifikat-ID: $CERT_ID" +echo "" + +# 4. Zertifikat abrufen +echo "4. Rufe Zertifikat ab..." +curl -s "$CA_URL/certificate/$CERT_ID" | jq -r '.certificate' > test.crt +echo "✓ Zertifikat gespeichert in test.crt" +echo "" + +# 5. Root-Zertifikat abrufen +echo "5. Rufe Root-Zertifikat ab..." +curl -s "$CA_URL/root" > root.crt +echo "✓ Root-Zertifikat gespeichert in root.crt" +echo "" + +# 6. Zertifikat-Details anzeigen +echo "6. Zertifikat-Details:" +openssl x509 -in test.crt -text -noout | head -20 +echo "" + +# 7. Zertifikat mit Root verifizieren +echo "7. Verifiziere Zertifikat mit Root-CA..." +if openssl verify -CAfile root.crt test.crt > /dev/null 2>&1; then + echo "✓ Zertifikat ist gültig!" +else + echo "✗ Zertifikat-Verifizierung fehlgeschlagen" +fi +echo "" + +echo "=== Test abgeschlossen ===" +echo "Dateien:" +echo " - test.key (Private Key)" +echo " - test.csr (Certificate Signing Request)" +echo " - test.crt (Signiertes Zertifikat)" +echo " - root.crt (Root-Zertifikat)" + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e47522b --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module certigo-dummy-ca + +go 1.21 + +require github.com/gorilla/mux v1.8.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7128337 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..9f3704e --- /dev/null +++ b/main.go @@ -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) + } +}