Files
smgw-pki/docs/architecture.md
T
2026-05-12 19:25:14 +02:00

199 lines
9.0 KiB
Markdown

# 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).