9.0 KiB
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 wiedays_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-4RequestCertificate-Envelope inkl.callbackIndicator=callback_possible, eindeutigermessageID, Base64-kodierter CSR im FeldcertReq.InitialConfigBuilder— Erzeugticonfig.xml(TR-03109-1) und ruft denHsmPortzur 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::runarbeitet mitanyhow::Result.- Keine
Result<_, String>in Ports — strukturierte Fehler sind testbar und matchbar.
Async-Modell
HsmPortist synchron, weil PKCS#11 nativ blockierend ist. Adapter rufttokio::task::spawn_blockingan den richtigen Stellen.- Alle anderen Ports sind
async_trait. - Eine Tokio-Runtime trägt Axum und Cron.
tokio-cron-schedulerlä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).
Umsetzungsreihenfolge
- Domain + Ports stehen — Code kompiliert.
- In-Memory-Adapter für jeden Port → erste Use-Case-Tests grün.
SoftHsmAdapter— riskantestes Stück, früh validieren.SoapRequestBuilder+ Mock-CA (Wiremock o.ä.).SqliteAdapter+ Migrationen.- Axum Router (Callback + GUI) + Cron-Wiring.
SmtpAdapterzuletzt.
Bewusste Nicht-Entscheidungen
- Kein WSDL-Codegen. Die
RequestCertificate-Surface ist klein genug, um per Hand mitquick-xmlzu bauen. Eingespart: ein zerbrechlicher Codegen-Schritt. - Kein DI-Framework.
Arc<dyn Trait>imAppStatereicht. - XML-DSig nicht in reinem Rust. Wir wrappen das
xmlsec1-CLI imSoftHsmAdapter(über Pipes), bis ein reifer Rust-Wrapper fürlibxmlsec1verfügbar ist. Dokumentiert inbsi-compliance.md.