init
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user