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

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.rsCertificate, CertificateRequest, CertificateUsage, reine Methoden wie days_until_expiry, is_expiring_within.
  • gateway.rsGateway.

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

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.