199 lines
9.0 KiB
Markdown
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).
|