This commit is contained in:
2026-05-12 19:25:14 +02:00
commit 0f3173d93e
93 changed files with 11865 additions and 0 deletions
+198
View File
@@ -0,0 +1,198 @@
# Architektur
## Leitprinzip — Hexagonale Architektur
Die Anwendung folgt dem Muster **Ports & Adapters** (Alistair Cockburn). Ziel
ist die strikte Trennung der fachlichen Logik (Domain) von technischer
Infrastruktur (HSM, CA, DB, SMTP, HTTP). Konsequenzen:
- Die Domäne ist frei von asynchroner Laufzeit, ORM-Typen, HTTP-Clients und
XML-Bibliotheken. Sie enthält reine Daten und reine Funktionen.
- Use-Cases ("Anwendungsdienste") orchestrieren die Domäne, indem sie Ports
konsumieren.
- Adapter sind austauschbar. Für Tests gibt es In-Memory-Adapter; für die
Lab-Umgebung gibt es SoftHSM-/SQLite-/SMTP-/SOAP-Adapter.
```
┌──────────────────────────────────────┐
│ Domain │
│ Certificate, Gateway, Policies │
└───────────────▲──────────────────────┘
┌────────────────────┴──────────────────────┐
│ Ports │
│ Inbound (UseCases) Outbound (Traits) │
└─────▲─────────────────────────▲───────────┘
│ │
┌────────────────┴─────┐ ┌─────────┴─────────────────┐
│ Inbound │ │ Outbound │
│ (treibt die App) │ │ (wird von der App │
│ │ │ getrieben) │
│ • Axum HTTP-Server │ │ • SoftHsmAdapter │
│ • Tokio-Cron │ │ • SubCaSoapAdapter │
│ │ │ • SqliteAdapter │
│ │ │ • SmtpAdapter │
│ │ │ • SystemClock │
└──────────────────────┘ └───────────────────────────┘
```
## Schichten
### Domain (`src/domain/`)
Keine externen Abhängigkeiten außer `time` (Datum/Zeit-Typen).
- `certificate.rs``Certificate`, `CertificateRequest`, `CertificateUsage`,
reine Methoden wie `days_until_expiry`, `is_expiring_within`.
- `gateway.rs``Gateway`.
### Ports (`src/ports/`)
Traits, die die Domäne nach außen exponiert bzw. von außen erwartet.
**Outbound-Ports** (`outbound.rs`) — was die Domäne von der Außenwelt braucht:
| Port | Zweck |
| ------------------- | ------------------------------------------- |
| `HsmPort` | Keypair-Generierung, CSR- und XML-Signatur. |
| `CertificateCaPort` | Asynchrone Zertifikatsanfrage an Sub-CA. |
| `StoragePort` | Persistenz von Zertifikaten + Pending Reqs. |
| `NotificationPort` | Versand von Alert-Mails. |
| `ClockPort` | Testbare Zeitquelle für Ablauflogik. |
Jeder Port hat seinen eigenen Fehlertyp (`HsmError`, `CaError`,
`StorageError`, `NotificationError`) via `thiserror`. Auf Use-Case-Ebene wird
auf `UseCaseError` aggregiert; auf App-Ebene auf `anyhow::Result`.
**Inbound-Ports** (`inbound.rs`) — was die Domäne anbietet:
- `RenewExpiringCertificates` — wird vom Cron getrieben.
- `HandleCaCallback` — wird vom HTTP-Webhook getrieben.
### Adapters (`src/adapters/`)
Konkrete Implementierungen der Outbound-Ports:
| Adapter | Crates | Anmerkung |
| -------------------- | ---------------------------- | ------------------------------------------------------ |
| `SoftHsmAdapter` | `cryptoki` | PKCS#11. Blocking-Aufrufe via `spawn_blocking`. |
| `SubCaSoapAdapter` | `reqwest` (rustls), `quick-xml` | mTLS-Client. SOAP per Hand (kleine Surface). |
| `SqliteAdapter` | `sqlx` | Migrationen unter `migrations/`. |
| `SmtpAdapter` | `lettre` | SMTP via Tokio + rustls. |
| `SystemClock` | `time` | Triviale `now()`-Implementierung. |
### Builders (`src/builders/`)
Kapseln das Erzeugen komplexer Payloads:
- `SoapRequestBuilder` — TR-03129-4 `RequestCertificate`-Envelope inkl.
`callbackIndicator=callback_possible`, eindeutiger `messageID`,
Base64-kodierter CSR im Feld `certReq`.
- `InitialConfigBuilder` — Erzeugt `iconfig.xml` (TR-03109-1) und ruft den
`HsmPort` zur Signatur (`iconfig.sig`) auf. Wichtig: Signatur erfolgt auf
den kanonischen (C14N) Bytes, **nicht** auf pretty-printed XML.
### Composition Root (`src/app.rs`)
`AppState` hält `Arc<dyn Port>` für alle Outbound-Ports. `app::run()` baut die
Adapter, wired sie in den AppState, startet Axum-Router und Tokio-Cron.
## Datenflüsse
### Zertifikatserneuerung (Cron-getrieben)
```
Cron ──► RenewExpiringCertificates::run(days=30)
├─► StoragePort::get_expiring_certificates(now, 30)
└─► für jedes Cert:
├─► HsmPort::generate_key_pair("gw-<id>-<usage>")
├─► CSR bauen, HsmPort::sign_csr(...)
├─► SoapRequestBuilder::build_request_certificate(...)
├─► CertificateCaPort::request_certificate(csr)
│ └─ liefert messageID
└─► StoragePort::save_pending_request(messageID, gateway_id)
```
Die CA antwortet synchron nur mit `returnCode=ok_syntax`. Das eigentliche
Zertifikat kommt asynchron über den Callback.
### Callback-Annahme (HTTP-getrieben)
```
CA ──► POST /pki/callback (mTLS, SOAP)
└─► Axum Handler
├─► mTLS-Client-Cert prüfen
├─► SOAP-Signatur prüfen
├─► messageID + certificateSeq extrahieren
└─► HandleCaCallback::handle(messageID, certificateSeq)
├─► StoragePort: pending → resolved
├─► StoragePort::update_certificate(...)
└─► NotificationPort::send_alert(...) bei Fehler
```
**Polling ist laut BSI verboten.** Der Callback ist der einzige Weg.
### Initial-Konfiguration
```
CLI/HTTP-Trigger ──► InitialConfigBuilder
├─► XML bauen (quick-xml)
├─► C14N
├─► HsmPort::sign_xml(GWADM_SIG_PRV-Label, c14n)
│ └─ liefert iconfig.sig (XML-DSig)
└─► tar(iconfig.xml, iconfig.sig) → iconfig.tar
```
## Querschnittsthemen
### Fehlerbehandlung
- Adapter werfen ihren eigenen `thiserror`-Typ.
- Use-Cases mappen auf `UseCaseError`.
- `main.rs` / `app::run` arbeitet mit `anyhow::Result`.
- Keine `Result<_, String>` in Ports — strukturierte Fehler sind testbar und
matchbar.
### Async-Modell
- `HsmPort` ist **synchron**, weil PKCS#11 nativ blockierend ist. Adapter
ruft `tokio::task::spawn_blocking` an den richtigen Stellen.
- Alle anderen Ports sind `async_trait`.
- Eine Tokio-Runtime trägt Axum und Cron. `tokio-cron-scheduler` läuft im
selben Runtime.
### Nebenläufigkeit der Cron-Jobs
`tokio-cron-scheduler` startet einen Job auch dann, wenn der vorherige Lauf
noch läuft. Erneuerungs-Job daher mit `Semaphore(1)` schützen, um doppelte
`RequestCertificate`-Aufrufe für dasselbe Gateway zu verhindern.
### Testbarkeit
- Domäne wird ohne Adapter getestet.
- Pro Port existiert ein In-Memory-Adapter unter `#[cfg(test)]`.
- Integrationstests gegen SoftHSM2 laufen in Docker (siehe
[`development.md`](development.md)).
## Umsetzungsreihenfolge
1. **Domain + Ports** stehen — Code kompiliert.
2. **In-Memory-Adapter** für jeden Port → erste Use-Case-Tests grün.
3. **`SoftHsmAdapter`** — riskantestes Stück, früh validieren.
4. **`SoapRequestBuilder` + Mock-CA** (Wiremock o.ä.).
5. **`SqliteAdapter`** + Migrationen.
6. **Axum** Router (Callback + GUI) + Cron-Wiring.
7. **`SmtpAdapter`** zuletzt.
## Bewusste Nicht-Entscheidungen
- **Kein WSDL-Codegen.** Die `RequestCertificate`-Surface ist klein genug,
um per Hand mit `quick-xml` zu bauen. Eingespart: ein zerbrechlicher
Codegen-Schritt.
- **Kein DI-Framework.** `Arc<dyn Trait>` im `AppState` reicht.
- **XML-DSig nicht in reinem Rust.** Wir wrappen das `xmlsec1`-CLI im
`SoftHsmAdapter` (über Pipes), bis ein reifer Rust-Wrapper für `libxmlsec1`
verfügbar ist. Dokumentiert in [`bsi-compliance.md`](bsi-compliance.md).
+141
View File
@@ -0,0 +1,141 @@
# BSI-Konformität
Mapping der Vorgaben aus den BSI-Richtlinien auf die Code-Stellen in diesem
Projekt. Quelle aller Anforderungen: **BSI TR-03129-4**, **BSI TR-03109-1**
und **SM-PKI Certificate Policy**.
## 1. TR-03129-4 — Schnittstelle zur Zertifizierungsstelle
### 1.1 Transportsicherheit (mTLS)
| Anforderung | Code-Stelle |
| ------------------------------------------------------------------ | -------------------------------------- |
| SOAP-Nachrichten enthalten **keine** Auth-Daten. | `builders/soap_req.rs` |
| Autorisierung + Verschlüsselung **ausschließlich** via mTLS. | `adapters/sub_ca.rs` |
| Client-Cert (EMT-Testzertifikat) konfiguriert in `reqwest`. | `SubCaSoapAdapter::new(...)` |
| Server-Cert der CA gegen vertrauten CA-Trust-Store verifiziert. | `SubCaSoapAdapter::new(...)` |
Konkret in `reqwest`:
```rust
reqwest::ClientBuilder::new()
.use_rustls_tls()
.identity(reqwest::Identity::from_pem(client_pem_bundle)?)
.add_root_certificate(reqwest::Certificate::from_pem(ca_pem)?)
.build()?
```
### 1.2 WSDL-Konformität
| Anforderung | Code-Stelle |
| -------------------------------------------------------- | ------------------------- |
| Envelope folgt der offiziellen BSI-WSDL. | `builders/soap_req.rs` |
| Operation `RequestCertificate` mit korrektem Namespace. | `SoapRequestBuilder::build_request_certificate` |
| CSR wird Base64-kodiert im Feld `certReq` übertragen. | dito |
Bewusste Entscheidung: **kein WSDL-Codegen**. Die Surface ist klein,
`quick-xml` reicht; Codegen-Crates für Rust sind unausgereift und brittle.
### 1.3 Asynchrone Callbacks (Polling-Verbot)
| Anforderung | Code-Stelle |
| ------------------------------------------------------ | -------------------------------------------------- |
| `callbackIndicator = callback_possible` im Request. | `SoapRequestBuilder::build_request_certificate` |
| Eindeutige `messageID` pro Anfrage. | `SoapRequestBuilder::message_id` |
| Synchroner Response enthält i.d.R. nur `returnCode`. | `SubCaSoapAdapter::request_certificate` |
| Webhook nimmt das fertige Zertifikat entgegen. | Axum-Handler (folgt; `POST /pki/callback`) |
| Zuordnung Callback → Request über `messageID`. | `StoragePort::save_pending_request` / Lookup |
| Zertifikatskette aus `certificateSeq` extrahieren. | Axum-Handler / `HandleCaCallback` |
**Sicherheitspunkt:** `messageID` allein ist **kein** Trust-Boundary. Der
Callback-Handler muss zusätzlich:
1. mTLS-Client-Cert der CA prüfen.
2. SOAP-Signatur der Nachricht prüfen.
Erst dann darf in der DB nachgeschlagen werden.
### 1.4 Datenfelder
| Feld | Inhalt | Quelle |
| ----------------- | ------------------------------------------ | ----------------------------------- |
| `certReq` | Base64(CSR-DER) — vom HSM erzeugt. | `HsmPort::sign_csr` |
| `messageID` | UUIDv4, persistiert in `pending_requests`. | `SoapRequestBuilder::message_id` |
| `callbackIndicator` | `callback_possible` | `SoapRequestBuilder` |
| `certificateSeq` | Antwort der CA via Callback. | Axum-Handler |
## 2. TR-03109-1 — Initial-Konfiguration
### 2.1 Dateiformat `iconfig.tar`
| Pflicht-Inhalt | Code-Stelle |
| ----------------------------------------------- | ------------------------------------ |
| `iconfig.xml` — Konfiguration. | `builders/iconfig.rs` |
| `iconfig.sig` — XML-DSig über `iconfig.xml`. | `builders/iconfig.rs` + `HsmPort` |
| Beide Dateien in einem Tar-Archiv verpackt. | `builders/iconfig.rs` (Tar-Stufe) |
### 2.2 Inhalt der `iconfig.xml`
Pflichtbestandteile:
- Admin-Zertifikate (öffentliche Teile).
- CA-Zertifikatskette.
- Netzwerkprofil (IP-Access für WAN).
- Kommunikationsprofil (TLS-Admin für WAN-Schnittstelle).
Generierung im `InitialConfigBuilder` via `quick-xml`. Eingangsdaten aus der
Domäne (`Gateway`) bzw. aus Konfig-Files.
### 2.3 Kryptografische Signatur `iconfig.sig`
| Anforderung | Code-Stelle |
| -------------------------------------------------------------------- | ------------------------------------ |
| Signatur mit `GWADM_SIG_PRV` (privater Signaturschlüssel des GWA). | `HsmPort::sign_xml` |
| Schlüssel liegt im HSM; Zugriff via PKCS#11. | `adapters/hsm.rs` (`cryptoki`) |
| Signatur über **kanonische** Bytes (XML-C14N). | `InitialConfigBuilder` |
**Implementierungs-Hinweis (XML-DSig):**
Reines Rust für XML-DSig ist nicht reif. Optionen:
1. `xmlsec1` CLI über `std::process::Command` aus dem Adapter aufrufen
(pragmatischer Start).
2. `libxmlsec1` über FFI binden.
3. C14N + Signatur manuell via `cryptoki` zusammensetzen (fehleranfällig
wegen exakter Canonicalization).
Wir starten mit Option 1 und kapseln das vollständig hinter `HsmPort::sign_xml`,
damit ein späterer Wechsel keinen Domänen-Code anfasst.
## 3. SM-PKI Certificate Policy
### 3.1 Schlüsselspeicherung
| Anforderung | Code-Stelle |
| ------------------------------------------------------------ | ------------------------- |
| Alle Teilnehmer-Schlüssel in HSMs ≥ "Security Level 1". | `adapters/hsm.rs` |
| SoftHSMv2 erfüllt SL1 **nur** für Entwicklung. | siehe Sicherheitshinweis |
In Produktion: zertifiziertes HSM (CC EAL4+) zwingend. Der `HsmPort` bleibt
identisch — nur das Backend tauscht.
### 3.2 Zertifikatslaufzeiten und Rotation
| Anforderung | Code-Stelle |
| -------------------------------------------------------- | ------------------------------------------ |
| Endentitäten/Sub-CAs in der Regel alle 2 Jahre. | Datenmodell `Certificate.not_after` |
| Erneuerung rechtzeitig vor Ablauf (Standard: 30 Tage). | `RenewExpiringCertificates::run(days=30)` |
| Asynchroner `RequestCertificate`-Flow. | siehe §1.3 |
| Testbare Zeitquelle. | `ClockPort` / `SystemClock` |
Das 30-Tage-Fenster ist Default, kein Gesetz — über Config einstellbar.
## 4. Offene Punkte
- Konkrete Werte der BSI-Namespace-URIs und Operationsnamen aus der aktuellen
WSDL-Version müssen in `builders/soap_req.rs` als Konstanten gepflegt
werden, sobald die WSDL aus dem BSI-Repo gezogen ist.
- Schema-Validierung der `iconfig.xml` gegen das BSI-XSD ist offen; sinnvoll
als zusätzlicher Adapter-Check vor dem Signieren.
- mTLS-Server (Axum für Callback) muss Client-Cert-Pinning auf die CA-Cert
durchsetzen — Konfiguration unter [`development.md`](development.md).
+153
View File
@@ -0,0 +1,153 @@
# Development Setup
Lokales Setup für die Lab-Umgebung. Ziel: Tool gegen einen SoftHSM2-Container
und eine Test-Sub-CA betreiben, ohne echte SMGW-Hardware.
## Voraussetzungen
- Rust ≥ 1.80 (`rustup`)
- Docker / Podman
- `xmlsec1` CLI (für die XML-DSig in `iconfig.sig`)
- OpenSSL CLI (für Testzertifikate)
Auf Debian/Ubuntu:
```bash
sudo apt install xmlsec1 libxmlsec1-dev openssl pkg-config
```
## Build & Run
```bash
cargo check
cargo run
```
Logs via `RUST_LOG`:
```bash
RUST_LOG=smgw_pki_automator=debug,info cargo run
```
## SoftHSMv2 als Container
`SoftHSMv2` per Docker laufen lassen. Beispiel (Pseudo-Compose, anpassen an
euer Setup):
```yaml
services:
softhsm:
image: ghcr.io/example/softhsm2:latest
volumes:
- ./tokens:/var/lib/softhsm/tokens
environment:
SOFTHSM2_CONF: /etc/softhsm2.conf
```
Token einmalig initialisieren:
```bash
softhsm2-util --init-token --slot 0 \
--label "smgw-lab" \
--pin 1234 --so-pin 5678
```
Im `app.rs`-Boot-Pfad wird der Adapter konstruiert mit:
```rust
SoftHsmAdapter::new("/usr/lib/softhsm/libsofthsm2.so", "1234")
```
Pfad zur PKCS#11-Lib hängt vom Container/OS ab.
## mTLS-Testzertifikate
Für die Kommunikation mit der Test-Sub-CA brauchen wir:
- **Client-Cert (EMT)** — wird im `reqwest`-Client als `Identity` geladen.
- **Server-Cert der CA** — wird als Trust-Root im Client geladen.
- **Eigenes Server-Cert** für den Axum-Callback-Endpunkt (mTLS-Server).
- **CA-Trust** für eingehende CA-Calls, damit das Client-Cert der CA gegen
diesen Trust verifiziert werden kann.
Test-PKI mit OpenSSL aufsetzen (vereinfacht):
```bash
mkdir -p certs && cd certs
# Test-Root
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
-keyout test-root.key -out test-root.crt \
-subj "/CN=smgw-lab-root"
# Client (EMT)
openssl req -newkey rsa:4096 -nodes -keyout emt.key -out emt.csr \
-subj "/CN=emt-client"
openssl x509 -req -in emt.csr -CA test-root.crt -CAkey test-root.key \
-CAcreateserial -out emt.crt -days 825
# Server (CA-Mock + Axum-Callback)
openssl req -newkey rsa:4096 -nodes -keyout server.key -out server.csr \
-subj "/CN=ca.test.local"
openssl x509 -req -in server.csr -CA test-root.crt -CAkey test-root.key \
-CAcreateserial -out server.crt -days 825
```
PEM-Bundle für `reqwest::Identity::from_pem`:
```bash
cat emt.crt emt.key > emt-bundle.pem
```
## Datenbank
SQLite, default `sqlite::memory:` im Stub-Boot. Für Persistenz Pfad setzen:
```bash
DATABASE_URL=sqlite://./data/smgw.db cargo run
```
Migrationen (sobald angelegt) via `sqlx`:
```bash
cargo install sqlx-cli --no-default-features --features sqlite,rustls
sqlx migrate run
```
## SMTP
Für lokales Testing `MailHog` oder `Mailpit`:
```bash
docker run -d -p 1025:1025 -p 8025:8025 axllent/mailpit
```
`SmtpAdapter::new("localhost", 1025)`.
## Test-Sub-CA
Optionen:
1. **Mock-Server** mit `wiremock` für Integrationstests — antwortet auf
`RequestCertificate` mit `ok_syntax` und triggert anschließend einen
HTTP-Call gegen den eigenen Callback-Endpunkt. Kein BSI-Compliance-Ersatz,
aber gut für CI.
2. **Echte Test-Sub-CA**-Instanz (sofern verfügbar) — Adresse + Test-EMT-
Zertifikate aus eurem Lab-Bestand einsetzen.
## Tests
```bash
cargo test # Unit + In-Memory-Integration
cargo test --features it # Integration mit SoftHSM-Container (folgt)
```
Konvention: Tests liegen neben dem Code (`#[cfg(test)] mod tests`). Adapter-
Tests, die externe Dienste brauchen, hinter Cargo-Feature `it` gaten.
## Code-Style
- `cargo fmt` vor jedem Commit.
- `cargo clippy --all-targets -- -D warnings` muss grün sein.
- Keine `Result<_, String>` in Produktionscode. Strukturierte Fehler via
`thiserror`.