Zum Inhalt springen

Fallstudie

Wallet-App mit klar getrennter Vertrauensgrenze

WalletPro trennt bewusst: private Schlüssel und Signatur bleiben auf dem Gerät, die Backend-API liefert Daten und leitet signierte Transaktionen an die Chain weiter.

Herausforderung

Eine Wallet-Anwendung lebt von einer Regel, die keine Verhandlungssache ist: private Schlüssel dürfen das Gerät nicht verlassen, und jede Transaktion muss lokal signiert werden. Alles, was diese Regel aufweicht — Cloud-Backups im Klartext, serverseitig gehaltene Seeds, „bequeme” Wiederherstellung per Passwort — verschiebt das Risiko auf den Betreiber und macht die App zu einem Ziel, das ganze Kassen bewegt statt einzelne Geräte.

Zugleich braucht eine ernsthafte Wallet ein Backend: Kursdaten, Token-Metadaten, RPC-Zugriffe, Broadcast an mehrere Chains, Missbrauchsschutz. Diese Backend-Rolle muss so eng geschnitten sein, dass sie zwar den Alltag beschleunigt, aber ausserhalb ihrer Grenzen nichts autorisiert. Genau diese Grenze — und ihre offene Dokumentation — steht bei WalletPro im Zentrum.

Zusätzlich sollen zwei native Clients (Android, iOS) und mehrere Ketten aus einem Repository heraus gepflegt werden, ohne dass wir jede Chain-Integration zweimal implementieren.

Ansatz

Fünf Prinzipien:

  • Signatur ausschliesslich auf dem Gerät — Seeds und private Schlüssel liegen im lokalen Keystore/Secure Enclave, nicht im Backend.
  • JWT als Anti-Missbrauchs-Token, nicht als Eigentumsbeweis — die API akzeptiert jeden authentifizierten Aufruf für öffentlich lesbare On-Chain-Daten, unabhängig davon, ob die Adresse zum Aufrufer gehört.
  • Broadcast kryptografisch autorisiert — signierte Transaktionen sind selbstbeweisend; das Backend muss nichts über den Besitzer wissen.
  • Ein Rust-Kern für Backend-Dienste — Cargo-Workspace mit mehreren Crates, damit wallet-api, wallet-worker und geteilte Bausteine dieselben Typen verwenden.
  • Offene Dokumentation der Grenzen — Audit-Reports und die OpenAPI-Beschreibung nennen die bewusste Entscheidung beim Namen, damit Scanner-Funde als „akzeptiert per Design” nachvollziehbar sind.

Was wir gebaut haben

Rust-Backend (services/, crates/, backend/)

Ein Cargo-Workspace mit wallet-api als HTTP-Dienst und weiteren Crates unter crates/. Der Dienst liefert Balances, Transaktionshistorie, Kursdaten und Token-Metadaten und bietet den Broadcast-Endpunkt für signierte Transaktionen. Cargo.toml und Cargo.lock sitzen an der Wurzel und binden alle Crates in einen gemeinsamen Build ein.

Android-Client (Kotlin, android/)

Nativer Android-Client mit lokaler Schlüsselverwaltung über den Android Keystore. Der CI-Workflow .github/workflows/android-tests.yml fährt bei jedem Push oder PR gegen android/ die Ziele :app:compileDebugKotlin, :app:testDebugUnitTest und :app:lintDebug auf Ubuntu mit Temurin JDK 17. Test- und Lint-Reports werden als Artefakte hochgeladen (testDebugUnitTest-reports, lint-results-debug).

iOS-Client (Swift, ios/)

Nativer iOS-Client mit Schlüsseln in der Secure Enclave. Die Chain-Integrationen folgen denselben Modellen wie auf Android; API-Verträge werden über eine gemeinsame OpenAPI-Beschreibung synchron gehalten.

Geteilter Mobile-Layer (mobile/)

Ein plattformnaher Layer für gemeinsame Datenmodelle und Hilfsfunktionen. Kette-spezifische Logik (Adressvalidierung, Gebührenberechnung, Explorer-URLs) landet dort einmal und wird sowohl von Android als auch von iOS konsumiert.

E2E- und Last-Tests (.maestro/, load-tests/)

Maestro-Flows für UI-E2E-Tests decken zentrale Pfade (Onboarding, Empfangen, Senden, Historie) ab. load-tests/ enthält Lasttests gegen das Rust-Backend, damit die API unter realistischen Anfragezahlen belastbar bleibt.

Infrastruktur & Monitoring (infra/, monitoring/, docker/)

Container-Definitionen und Monitoring-Konfiguration für das Backend, inklusive Dockerfile und .env.docker.example als Vorlage. secrets/ ist als Struktur vorhanden, Werte werden nicht eingecheckt (.gitleaks.toml und Trivy-Reports halten das durch).

Sicherheits- und Auditberichte

Mehrere Audit-Reports (AUDIT_REPORT_2026-02-21.md, DETAILED_AUDIT_REPORT_2026-02-21.md, SECURITY_AND_INTEGRATION_AUDIT_2026-02-21.md, MOBILE_APP_AUDIT_2026-02-21.md) dokumentieren Sicherheitsfindungen und Umsetzungsstand. SECURITY_FIXES_APPLIED.md protokolliert, welche Findings tatsächlich adressiert wurden.

Architektur

WalletPro trennt strikt in Gerät und Backend. Auf dem Gerät liegen Seed und private Schlüssel im plattformeigenen Keystore (Android Keystore, iOS Secure Enclave). Signaturen entstehen dort, nicht im Netz.

Das Rust-Backend (wallet-api) übernimmt drei Rollen: (1) Lese-API für öffentlich verfügbare On-Chain-Daten (Balances, Historien, Preise, Token-Metadaten), (2) Broadcast-Endpunkt, der bereits signierte Transaktionspayloads an die zuständigen RPC-Knoten weiterleitet, (3) leichter Missbrauchsschutz (Rate Limiting, JWT-basierte Authentifizierung) über ein /api/v1/auth/*-Challenge-Response-Verfahren.

Das JWT auf den Datenrouten ist ausdrücklich kein Eigentumsnachweis: Es bestätigt nur, dass der Aufrufer die Challenge bestanden hat, und ist nicht an eine bestimmte Adresse gebunden. Jede authentifizierte Aufruferin darf Balances und Historie zu jeder unterstützten Adresse abfragen — dieselben Daten sind ohnehin auf jedem öffentlichen RPC oder Block-Explorer verfügbar. Der POST /api/v1/transactions/broadcast-Endpunkt ist kryptografisch durch die Signatur der Payload autorisiert; das JWT gewährt keinerlei Verfügungsgewalt über eine Adresse.

Diese Grenze ist im Repository dokumentiert (README, ARCHITECTURE.md, OpenAPI-Beschreibung in services/wallet-api/src/openapi.rs), damit automatische Scanner, die die Datenrouten als „broken object-level authorization” markieren würden, den bewussten Entwurf als solchen erkennen.

Auf dem Client teilen sich Android und iOS gemeinsame Datenmodelle über eine OpenAPI-generierte Schnittstelle und einen dünnen mobile/-Layer für Ketten-Logik, sodass die Chain-Integration nur an einer Stelle geführt wird.

Zahlen & Fakten

MetrikWert
Backend-SpracheRust (Cargo-Workspace)
Backend-Dienstewallet-api, wallet-worker, geteilte Crates unter crates/
ClientsAndroid (Kotlin) · iOS (Swift)
Geteilte ModelleOpenAPI + mobile/-Layer
SignaturAusschliesslich clientseitig (Android Keystore, iOS Secure Enclave)
Auth-ModellJWT als Anti-Missbrauchs-Token; Broadcast kryptografisch signiert
CIandroid-tests.yml — Kotlin-Compile, Unit-Tests, Lint auf Ubuntu + Temurin JDK 17
E2E / LastMaestro (.maestro/) + Lasttests (load-tests/)
Sicherheits-WerkzeugeGitleaks, Trivy (.trivy-reports/)

Was wir gelernt haben

Klare Grenzen sind wertvoller als „starke” Backend-Rollen. Ein JWT, das mehr behauptet, als es beweisen kann, führt zu Sicherheitsversprechen, die nicht eingehalten werden. Es ist ehrlicher, das Backend als Datenkanal zu benennen und Broadcast per Signatur autorisieren zu lassen.

Dokumentation als Teil der Architektur. Ein Scanner-Finding zu OWASP API #1 lässt sich nicht wegdiskutieren — aber es lässt sich einordnen. Die Aufnahme der Begründung in README und OpenAPI-Beschreibung spart in Reviews und Kundengesprächen Stunden und schützt vor Ad-hoc-„Fixes”, die die Architektur aufweichen würden.

Cargo-Workspaces skalieren gut für mehrere Dienste. Statt jeden Dienst als isoliertes Crate zu versionieren, liefert ein Workspace einheitliche Abhängigkeiten und einen gemeinsamen Build. Das reduziert Reibung, wenn ein Fix in einer geteilten Crate mehrere Dienste erreichen soll.

CI muss den Hauptpfad tatsächlich absichern. Der Android-Workflow läuft nicht nur Unit-Tests, sondern zwingt auch zu einem erfolgreichen compileDebugKotlin und einem sauberen lintDebug. So bleiben Refactorings am Client rückführbar, statt sich in schweigenden Warnings zu verlieren.

Nächste Schritte

  • Analogen CI-Workflow für iOS aufsetzen (Xcode-Build, Unit-Tests) und in dieselbe Reports-Struktur einhängen.
  • OpenAPI-Vertrag als Single Source of Truth stärken und generierte SDKs für Android und iOS ausrollen.
  • Chain-Integrationen unter mobile/ weiter konsolidieren, damit neue Ketten mit klaren, testbaren Bausteinen angebunden werden.

Verwandt

Zusammenarbeit

Ähnliche Arbeit im Sinn?

Lassen Sie uns kurz sprechen — kein Verkaufsgespräch.