# 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` 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--") ├─► 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` 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).