Get your free and exclusive 80-page Banking Passkey Report
Back to Overview

So erstellst du einen Verifier für digitale Nachweise (Entwickler-Guide)

In diesem Entwickler-Guide bauen wir Schritt für Schritt einen Verifier für digitale Nachweise mit Next.js, OpenID4VP und ISO mDoc. Wir zeigen, wie ein Verifier mobile Führerscheine und andere digitale Nachweise anfordern, empfangen und validieren kann.

Amine

Created: August 20, 2025

Updated: August 21, 2025

Blog-Post-Header-Image

See the original blog version in English here.

DigitalCredentialsDemo Icon

Want to experience digital credentials in action?

Try Digital Credentials

1. Einführung#

Der Online-Nachweis von Identitäten ist eine ständige Herausforderung. Das führt dazu, dass wir uns auf Passwörter verlassen und sensible Dokumente über unsichere Kanäle teilen. Für Unternehmen wird die Identitätsprüfung dadurch zu einem langsamen, teuren und betrugsanfälligen Prozess. Digitale Nachweise (Digital Credentials) bieten einen neuen Ansatz, der den Nutzern die Kontrolle über ihre Daten zurückgibt. Sie sind das digitale Äquivalent einer physischen Wallet, die von einem Führerschein bis zum Universitätsabschluss alles enthalten kann – aber mit den zusätzlichen Vorteilen, kryptografisch sicher, datenschutzfreundlich und sofort überprüfbar zu sein.

Dieser Guide ist ein praktisches Schritt-für-Schritt-Tutorial für Entwickler, um einen Verifier für digitale Nachweise zu bauen. Obwohl die Standards existieren, gibt es kaum Anleitungen zur Implementierung. Dieses Tutorial füllt diese Lücke. Wir zeigen, wie man einen Verifier mit der nativen Digital Credential API des Browsers, OpenID4VP für das Präsentationsprotokoll und ISO mDoc (z. B. für den mobilen Führerschein) als Nachweisformat erstellt.

Das Endergebnis wird eine einfache, aber funktionale Next.js-Anwendung sein, die einen digitalen Nachweis von einer kompatiblen mobilen Wallet anfordern, empfangen und verifizieren kann.

Hier ist ein kurzer Blick auf die fertige Anwendung in Aktion. Der Prozess besteht aus vier Hauptschritten:

Schritt 1: Startseite Der User landet auf der Startseite und klickt auf „Mit digitaler Identität verifizieren“, um den Prozess zu starten.

Schritt 2: Vertrauensabfrage Der Browser fragt den User um sein Einverständnis. Der User klickt auf „Weiter“, um fortzufahren.

Schritt 3: QR-Code-Scan Ein QR-Code wird angezeigt, den der User mit seiner kompatiblen Wallet-Anwendung scannt.

Schritt 4: Entschlüsselter Nachweis Nach erfolgreicher Verifizierung zeigt die Anwendung die entschlüsselten Nachweisdaten an.

1.1 Wie es funktioniert#

Die Magie hinter digitalen Nachweisen liegt in einem einfachen, aber leistungsstarken „Vertrauensdreieck“-Modell, das drei Hauptakteure umfasst:

  • Aussteller (Issuer): Eine vertrauenswürdige Instanz (z. B. eine Regierungsbehörde, eine Universität oder eine Bank), die einen Nachweis kryptografisch signiert und an einen User ausstellt.
  • Inhaber (Holder): Der User, der den Nachweis erhält und ihn sicher in einer persönlichen digitalen Wallet auf seinem Gerät speichert.
  • Verifier: Eine Anwendung oder ein Dienst, der den Nachweis des Users überprüfen muss.

Wenn ein User auf einen Dienst zugreifen möchte, präsentiert er den Nachweis aus seiner Wallet. Der Verifier kann dann sofort dessen Echtheit überprüfen, ohne den ursprünglichen Aussteller direkt kontaktieren zu müssen.

1.2 Warum Verifier unerlässlich sind (und warum du hier bist)#

Damit dieses dezentrale Identitäts-Ökosystem erfolgreich sein kann, ist die Rolle des Verifiers absolut entscheidend. Sie sind die Gatekeeper dieser neuen Vertrauensinfrastruktur, diejenigen, die die Nachweise nutzen und sie in der realen Welt einsetzbar machen. Wie das nachstehende Diagramm zeigt, vervollständigt ein Verifier das Vertrauensdreieck, indem er einen Nachweis vom Inhaber anfordert, empfängt und validiert.

Wenn du Entwickler bist, ist das Erstellen eines Dienstes zur Durchführung dieser Verifizierung eine grundlegende Fähigkeit für die nächste Generation sicherer und nutzerzentrierter Anwendungen. Dieser Guide wurde entwickelt, um dich genau durch diesen Prozess zu führen. Wir werden alles behandeln, was du wissen musst, um deinen eigenen Verifier für verifizierbare Nachweise zu bauen, von den Kernkonzepten und Standards bis hin zu den schrittweisen Implementierungsdetails zur Validierung von Signaturen und zur Überprüfung des Nachweisstatus.

Möchtest du direkt loslegen? Du findest das komplette, fertige Projekt für dieses Tutorial auf GitHub. Klone es einfach und probiere es selbst aus: https://github.com/corbado/digital-credentials-example

Fangen wir an.

2. Voraussetzungen, um einen Verifier zu bauen#

Bevor du beginnst, stelle sicher, dass du Folgendes hast:

  1. Grundlegendes Verständnis von Digital Credentials und mdoc
    • Dieses Tutorial konzentriert sich auf das ISO mDoc-Format (z. B. für mobile Führerscheine) und behandelt keine anderen Formate wie W3C Verifiable Credentials (VCs). Kenntnisse der grundlegenden Konzepte von mdoc sind hilfreich.
  2. Docker und Docker Compose
    • Unser Projekt verwendet eine MySQL-Datenbank in einem Docker-Container, um den OIDC-Sitzungsstatus zu verwalten. Stelle sicher, dass beides installiert ist und läuft.
  3. Gewähltes Protokoll: OpenID4VP
    • Wir werden das OpenID4VP-Protokoll (OpenID for Verifiable Presentations) für den Austausch von Nachweisen verwenden.
  4. Tech-Stack bereit
    • Verwende TypeScript (Node.js) für die Backend-Logik.
    • Verwende Next.js sowohl für das Backend (API-Routen) als auch für das Frontend (UI).
    • Wichtige Bibliotheken: CBOR-Dekodierungsbibliotheken für das Parsen von mdoc und ein MySQL-Client.
  5. Test-Credentials und Wallet
    • Wir werden die CMWallet für Android verwenden, die OpenID4VP-Anfragen versteht und mdoc-Nachweise präsentieren kann.
  6. Grundkenntnisse in Kryptografie
    • Verständnis von digitalen Signaturen und Public-/Private-Key-Konzepten im Zusammenhang mit mdoc- und OIDC-Flows.

Wir werden nun jede dieser Voraussetzungen im Detail durchgehen, beginnend mit den Standards und Protokollen, die diesem mdoc-basierten Verifier zugrunde liegen.

2.1 Protokoll-Entscheidungen#

Unser Verifier ist für Folgendes ausgelegt:

Standard / ProtokollBeschreibung
W3C VCDas W3C Verifiable Credentials Data Model. Es definiert die Standardstruktur für digitale Nachweise, einschließlich Claims, Metadaten und Proofs.
SD-JWTSelective Disclosure for JWTs. Ein Format für VCs, das auf JSON Web Tokens basiert und es Inhabern ermöglicht, nur bestimmte Claims aus einem Nachweis selektiv preiszugeben, was den Datenschutz verbessert.
ISO mDocISO/IEC 18013-5. Der internationale Standard für mobile Führerscheine (mDLs) und andere mobile IDs, der Datenstrukturen und Kommunikationsprotokolle für den Offline- und Online-Einsatz definiert.
OpenID4VPOpenID for Verifiable Presentations. Ein interoperables Präsentationsprotokoll, das auf OAuth 2.0 aufbaut. Es definiert, wie ein Verifier Nachweise anfordert und die Wallet eines Inhabers sie präsentiert.

Für dieses Tutorial verwenden wir speziell:

  • OpenID4VP als Protokoll für die Anforderung und den Empfang von Nachweisen.
  • ISO mDoc als Nachweisformat (z. B. für mobile Führerscheine).

Hinweis zum Umfang: Obwohl wir W3C VC und SD-JWT kurz vorstellen, um einen breiteren Kontext zu schaffen, implementiert dieses Tutorial ausschließlich ISO mDoc-Nachweise über OpenID4VP. W3C-basierte VCs sind nicht Teil dieses Beispiels.

2.1.1 ISO mDoc (Mobile Document)#

Der Standard ISO/IEC 18013-5 mDoc definiert die Struktur und Kodierung für mobile Dokumente wie mobile Führerscheine (mDLs). mDoc-Nachweise sind CBOR-kodiert, kryptografisch signiert und können zur Verifizierung digital vorgelegt werden. Unser Verifier wird sich darauf konzentrieren, diese mdoc-Nachweise zu dekodieren und zu validieren.

2.1.2 OpenID4VP (OpenID for Verifiable Presentations)#

OpenID4VP ist ein interoperables Protokoll zur Anforderung und Präsentation von digitalen Nachweisen, das auf OAuth 2.0 und OpenID Connect aufbaut. In dieser Implementierung wird OpenID4VP verwendet, um:

  • den Prozess der Nachweispräsentation zu initiieren (über QR-Code oder Browser-API)
  • den mdoc-Nachweis von der Wallet des Users zu erhalten
  • einen sicheren, zustandsbehafteten und datenschutzfreundlichen Austausch von Nachweisen zu gewährleisten

2.2 Tech-Stack-Entscheidungen#

Nachdem wir ein klares Verständnis der Standards und Protokolle haben, müssen wir den richtigen Tech-Stack für den Bau unseres Verifiers auswählen. Unsere Entscheidungen sind auf Robustheit, Entwicklererfahrung und Kompatibilität mit dem modernen Web-Ökosystem ausgelegt.

2.2.1 Sprache: TypeScript#

Wir werden TypeScript sowohl für unseren Frontend- als auch für unseren Backend-Code verwenden. Als Superset von JavaScript fügt es statische Typisierung hinzu, was hilft, Fehler frühzeitig zu erkennen, die Codequalität zu verbessern und komplexe Anwendungen einfacher zu verwalten. In einem sicherheitssensiblen Kontext wie der Nachweisverifizierung ist Typsicherheit ein enormer Vorteil.

2.2.2 Framework: Next.js#

Next.js ist unser Framework der Wahl, da es eine nahtlose, integrierte Erfahrung für die Erstellung von Full-Stack-Anwendungen bietet.

  • Für das Frontend: Wir werden Next.js mit React verwenden, um die Benutzeroberfläche zu erstellen, auf der der Verifizierungsprozess initiiert wird (z. B. durch Anzeige eines QR-Codes).
  • Für das Backend: Wir werden Next.js API Routes nutzen, um die serverseitigen Endpunkte zu erstellen. Diese Endpunkte sind dafür verantwortlich, gültige OpenID4VP-Anfragen zu erstellen und als redirect_uri zu fungieren, um die endgültige Antwort von der CMWallet sicher zu empfangen und zu verifizieren.

2.2.3 Wichtige Bibliotheken#

Unsere Implementierung stützt sich auf eine spezifische Reihe von Bibliotheken für Frontend und Backend:

  • next: Das Next.js-Framework, das sowohl für Backend-API-Routen als auch für das Frontend-UI verwendet wird.
  • react und react-dom: Sorgen für die Funktionalität der Frontend-Benutzeroberfläche.
  • cbor-web: Wird verwendet, um CBOR-kodierte mdoc-Nachweise in nutzbare JavaScript-Objekte zu dekodieren.
  • mysql2: Stellt die MySQL-Datenbankverbindung zum Speichern von Challenges und Verifizierungssitzungen bereit.
  • uuid: Eine Bibliothek zur Erzeugung eindeutiger Challenge-Strings (Nonces).
  • @types/uuid: TypeScript-Typen für die UUID-Generierung.

Hinweis zu openid-client: Fortgeschrittenere, produktionsreife Verifier könnten die Bibliothek openid-client verwenden, um das OpenID4VP-Protokoll direkt im Backend zu handhaben und Funktionen wie eine dynamische redirect_uri zu ermöglichen. In einem servergesteuerten OpenID4VP-Flow mit einer redirect_uri würde openid-client verwendet, um vp_token-Antworten direkt zu parsen und zu validieren. Für dieses Tutorial verwenden wir einen einfacheren, browservermittelten Flow, der dies nicht erfordert, was den Prozess verständlicher macht.

Dieser Tech-Stack gewährleistet eine robuste, typsichere und skalierbare Verifier-Implementierung, die sich auf die Digital Credential API des Browsers und das ISO mDoc-Nachweisformat konzentriert.

2.3 Test-Wallet und -Nachweise besorgen#

Um deinen Verifier zu testen, benötigst du eine mobile Wallet, die mit der Digital Credential API des Browsers interagieren kann.

Wir werden die CMWallet verwenden, eine robuste, OpenID4VP-kompatible Test-Wallet für Android.

So installierst du CMWallet (Android):

  1. Lade die APK-Datei über den obigen Link direkt auf dein Android-Gerät herunter.
  2. Öffne auf deinem Gerät Einstellungen > Sicherheit.
  3. Aktiviere „Unbekannte Apps installieren“ für den Browser, mit dem du die Datei heruntergeladen hast.
  4. Finde die heruntergeladene APK in deinem „Downloads“-Ordner und tippe darauf, um die Installation zu starten.
  5. Folge den Anweisungen auf dem Bildschirm, um die Installation abzuschließen.
  6. Öffne die CMWallet. Sie ist bereits mit Test-Nachweisen vorinstalliert und bereit für den Verifizierungsprozess.

Hinweis: Installiere APK-Dateien nur aus Quellen, denen du vertraust. Der bereitgestellte Link stammt aus dem offiziellen Projekt-Repository.

2.4 Kryptografie-Kenntnisse#

Bevor wir in die Implementierung eintauchen, ist es wichtig, die kryptografischen Konzepte zu verstehen, die verifizierbaren Nachweisen zugrunde liegen. Das ist es, was sie „verifizierbar“ und vertrauenswürdig macht.

2.4.1 Digitale Signaturen: Die Grundlage des Vertrauens#

Im Kern ist ein Verifiable Credential eine Reihe von Claims (wie Name, Geburtsdatum usw.), die von einem Aussteller digital signiert wurden. Eine digitale Signatur bietet zwei entscheidende Garantien:

  • Authentizität: Sie beweist, dass der Nachweis tatsächlich vom Aussteller und nicht von einem Betrüger erstellt wurde.
  • Integrität: Sie beweist, dass der Nachweis seit seiner Signierung nicht verändert oder manipuliert wurde.

2.4.2 Public-Key-Kryptografie#

Digitale Signaturen werden mittels Public-Key-Kryptografie (auch asymmetrische Kryptografie genannt) erstellt. So funktioniert es in unserem Kontext:

  1. Der Aussteller hat ein Schlüsselpaar: einen privaten Schlüssel, der geheim gehalten wird, und einen öffentlichen Schlüssel, der allen zur Verfügung gestellt wird (normalerweise über sein DID-Dokument).
  2. Signieren: Wenn ein Aussteller einen Nachweis erstellt, verwendet er seinen privaten Schlüssel, um eine eindeutige digitale Signatur für diese spezifischen Nachweisdaten zu erzeugen.
  3. Verifizierung: Wenn unser Verifier den Nachweis erhält, verwendet er den öffentlichen Schlüssel des Ausstellers, um die Signatur zu überprüfen. Wenn die Prüfung erfolgreich ist, weiß der Verifier, dass der Nachweis authentisch ist und nicht manipuliert wurde. Jede Änderung der Nachweisdaten würde die Signatur ungültig machen.

Hinweis zu DIDs: In diesem Tutorial lösen wir die Schlüssel der Aussteller nicht über DIDs auf. In der Produktion würden Aussteller ihre öffentlichen Schlüssel typischerweise über DIDs oder andere autoritative Endpunkte bereitstellen, die der Verifier zur kryptografischen Validierung verwenden würde.

2.4.3 Verifizierbare Nachweise als JWTs#

Verifizierbare Nachweise werden oft als JSON Web Tokens (JWTs) formatiert. Ein JWT ist eine kompakte, URL-sichere Methode, um Claims darzustellen, die zwischen zwei Parteien übertragen werden. Ein signiertes JWT (auch als JWS bekannt) besteht aus drei Teilen, die durch Punkte (.) getrennt sind:

  • Header: Enthält Metadaten über das Token, wie den verwendeten Signaturalgorithmus (alg).
  • Payload: Enthält die eigentlichen Claims des Verifiable Credential (vc-Claim), einschließlich des issuer, credentialSubject usw.
  • Signatur: Die vom Aussteller erzeugte digitale Signatur, die den Header und die Payload abdeckt.
// Beispiel für eine JWT-Struktur [Header].[Payload].[Signatur]

Hinweis: JWT-basierte Verifiable Credentials sind nicht Gegenstand dieses Blog-Beitrags. Diese Implementierung konzentriert sich auf ISO mDoc-Nachweise und OpenID4VP, nicht auf W3C Verifiable Credentials oder JWT-basierte Nachweise.

2.4.4 Die verifizierbare Präsentation: Der Besitznachweis#

Es reicht nicht aus, dass ein Verifier weiß, dass ein Nachweis gültig ist; er muss auch wissen, dass die Person, die den Nachweis präsentiert, der rechtmäßige Inhaber ist. Dies verhindert, dass jemand einen gestohlenen Nachweis verwendet.

Dies wird durch eine Verifiable Presentation (VP) gelöst. Eine VP ist eine Hülle um einen oder mehrere VCs, die vom Inhaber selbst signiert ist.

Der Ablauf ist wie folgt:

  1. Der Verifier bittet den User, einen Nachweis zu präsentieren.
  2. Die Wallet des Users erstellt eine Verifiable Presentation, bündelt die erforderlichen Nachweise darin und signiert die gesamte Präsentation mit dem privaten Schlüssel des Inhabers.
  3. Die Wallet sendet diese signierte VP an den Verifier.

Unser Verifier muss dann zwei separate Signaturprüfungen durchführen:

  1. Nachweise verifizieren: Überprüfe die Signatur auf jedem VC innerhalb der Präsentation mit dem öffentlichen Schlüssel des Ausstellers. (Beweist, dass der Nachweis echt ist).
  2. Präsentation verifizieren: Überprüfe die Signatur auf der VP selbst mit dem öffentlichen Schlüssel des Inhabers. (Beweist, dass die präsentierende Person der Besitzer ist).

Diese zweistufige Prüfung gewährleistet sowohl die Authentizität des Nachweises als auch die Identität der Person, die ihn präsentiert, und schafft so ein robustes und sicheres Vertrauensmodell.

Hinweis: Das Konzept der Verifiable Presentations, wie es im W3C VC-Ökosystem definiert ist, ist nicht Gegenstand dieses Blog-Beitrags. Der Begriff Verifiable Presentation bezieht sich hier auf die OpenID4VP vp_token-Antwort, die sich ähnlich wie eine W3C VP verhält, aber auf ISO mDoc-Semantik anstelle des JSON-LD-Signaturmodells von W3C basiert. Dieser Guide konzentriert sich auf ISO mDoc-Nachweise und OpenID4VP, nicht auf W3C Verifiable Presentations oder deren Signaturvalidierung.

3. Architekturübersicht#

Unsere Verifier-Architektur nutzt die eingebaute Digital Credential API des Browsers als sicheren Vermittler, um unsere Webanwendung mit der mobilen CMWallet des Users zu verbinden. Dieser Ansatz vereinfacht den Ablauf, da der Browser die native QR-Code-Anzeige und die Kommunikation mit der Wallet übernimmt.

  • Frontend (Next.js & React): Eine schlanke, nutzerorientierte Website. Ihre Aufgabe ist es, ein Anfrageobjekt von unserem Backend abzurufen, es an die navigator.credentials.get() API des Browsers zu übergeben, das Ergebnis zu empfangen und es zur Verifizierung an unser Backend weiterzuleiten.
  • Backend (Next.js API Routes): Das Kraftpaket des Verifiers. Es generiert ein gültiges Anfrageobjekt für die Browser-API und stellt einen Endpunkt bereit, um die Nachweispräsentation vom Frontend zur endgültigen Validierung zu empfangen.
  • Browser (Credential API): Der Vermittler. Er empfängt das Anfrageobjekt von unserem Frontend, versteht das openid4vp-Protokoll und generiert nativ einen QR-Code. Dann wartet er auf die Antwort der Wallet.
  • CMWallet (Mobile App): Die Wallet des Users. Sie scannt den QR-Code, verarbeitet die Anfrage, holt die Zustimmung des Users ein und sendet die signierte Antwort zurück an den Browser.

Hier ist ein Sequenzdiagramm, das den vollständigen und korrekten Ablauf veranschaulicht:

Der Ablauf erklärt:

  1. Initiierung: Der User klickt auf den „Verifizieren“-Button in unserem Frontend.
  2. Anfrageobjekt: Das Frontend ruft unser Backend (/api/verify/start) auf, das ein Anfrageobjekt mit der Abfrage und einer Nonce generiert und zurückgibt.
  3. Browser-API-Aufruf: Das Frontend ruft navigator.credentials.get() mit dem Anfrageobjekt auf.
  4. Nativer QR-Code: Der Browser erkennt die openid4vp-Protokollanfrage und zeigt nativ einen QR-Code an. Das .get()-Promise ist nun ausstehend.

Hinweis: Dieser QR-Code-Flow findet in Desktop-Browsern statt. In mobilen Browsern (Android Chrome mit aktiviertem experimentellem Flag) kann der Browser direkt mit kompatiblen Wallets auf demselben Gerät kommunizieren, wodurch das Scannen von QR-Codes entfällt. Um diese Funktion in Android Chrome zu aktivieren, navigiere zu chrome://flags#web-identity-digital-credentials und setze das Flag auf „Enabled“.

  1. Scannen & Präsentieren: Der User scannt den QR-Code mit der CMWallet. Die Wallet holt die Zustimmung des Users ein und sendet die Verifiable Presentation zurück an den Browser.
  2. Promise-Auflösung: Der Browser empfängt die Antwort, und das ursprüngliche .get()-Promise im Frontend wird schließlich aufgelöst und liefert die Präsentations-Payload.
  3. Backend-Verifizierung: Das Frontend sendet die Präsentations-Payload per POST an den /api/verify/finish-Endpunkt unseres Backends. Das Backend validiert die Nonce und den Nachweis.
  4. Ergebnis: Das Backend gibt eine endgültige Erfolgs- oder Fehlermeldung an das Frontend zurück, das die Benutzeroberfläche aktualisiert.

4. Den Verifier bauen#

Nachdem wir nun ein solides Verständnis der Standards, Protokolle und des Architekturflusses haben, können wir mit dem Bau unseres Verifiers beginnen.

Folge mit oder verwende den fertigen Code

Wir werden nun Schritt für Schritt die Einrichtung und Code-Implementierung durchgehen. Wenn du lieber direkt zum fertigen Produkt springen möchtest, kannst du das vollständige Projekt aus unserem GitHub-Repository klonen und lokal ausführen.

git clone https://github.com/corbado/digital-credentials-example.git

4.1 Projekt einrichten#

Zuerst initialisieren wir ein neues Next.js-Projekt, installieren die notwendigen Abhängigkeiten und starten unsere Datenbank.

4.1.1 Next.js-App initialisieren#

Öffne dein Terminal, navigiere zu dem Verzeichnis, in dem du dein Projekt erstellen möchtest, und führe den folgenden Befehl aus. Wir verwenden für dieses Projekt den App Router, TypeScript und Tailwind CSS.

npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm

Dieser Befehl erstellt eine neue Next.js-Anwendung in deinem aktuellen Verzeichnis.

4.1.2 Abhängigkeiten installieren#

Als Nächstes müssen wir die Bibliotheken installieren, die sich um die CBOR-Dekodierung, Datenbankverbindungen und die UUID-Generierung kümmern.

npm install cbor-web mysql2 uuid @types/uuid

Dieser Befehl installiert:

  • cbor-web: Zum Dekodieren der mdoc-Nachweis-Payload.
  • mysql2: Der MySQL-Client für unsere Datenbank.
  • uuid: Zur Erzeugung eindeutiger Challenge-Strings.
  • @types/uuid: TypeScript-Typen für die uuid-Bibliothek.

4.1.3 Datenbank starten#

Unser Backend benötigt eine MySQL-Datenbank, um OIDC-Sitzungsdaten zu speichern und sicherzustellen, dass jeder Verifizierungsablauf sicher und zustandsbehaftet ist. Wir haben eine docker-compose.yml-Datei beigefügt, um dies zu vereinfachen.

Wenn du das Repository geklont hast, kannst du einfach docker-compose up -d ausführen. Wenn du von Grund auf neu beginnst, erstelle eine Datei namens docker-compose.yml mit dem folgenden Inhalt:

services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: rootpassword MYSQL_DATABASE: digital_credentials MYSQL_USER: app_user MYSQL_PASSWORD: app_password ports: - "3306:3306" volumes: - mysql_data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] timeout: 20s retries: 10 volumes: mysql_data:

Dieses Docker-Compose-Setup erfordert auch ein SQL-Initialisierungsskript. Erstelle ein Verzeichnis namens sql und darin eine Datei namens init.sql mit dem folgenden Inhalt, um die notwendigen Tabellen einzurichten:

-- Create database if not exists CREATE DATABASE IF NOT EXISTS digital_credentials; USE digital_credentials; -- Table for storing challenges CREATE TABLE IF NOT EXISTS challenges ( id VARCHAR(36) PRIMARY KEY, challenge VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used BOOLEAN DEFAULT FALSE, INDEX idx_challenge (challenge), INDEX idx_expires_at (expires_at) ); -- Table for storing verification sessions CREATE TABLE IF NOT EXISTS verification_sessions ( id VARCHAR(36) PRIMARY KEY, challenge_id VARCHAR(36), status ENUM('pending', 'verified', 'failed', 'expired') DEFAULT 'pending', presentation_data JSON, verified_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (challenge_id) REFERENCES challenges(id) ON DELETE CASCADE, INDEX idx_challenge_id (challenge_id), INDEX idx_status (status) ); -- Table for storing verified credentials data (optional) CREATE TABLE IF NOT EXISTS verified_credentials ( id VARCHAR(36) PRIMARY KEY, session_id VARCHAR(36), credential_type VARCHAR(255), issuer VARCHAR(255), subject VARCHAR(255), claims JSON, verified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (session_id) REFERENCES verification_sessions(id) ON DELETE CASCADE, INDEX idx_session_id (session_id), INDEX idx_credential_type (credential_type) );

Sobald beide Dateien vorhanden sind, öffne dein Terminal im Projektstammverzeichnis und führe aus:

docker-compose up -d

Dieser Befehl startet einen MySQL-Container im Hintergrund.

4.2 Architekturübersicht der Next.js-App#

Unsere Next.js-Anwendung ist so strukturiert, dass die Zuständigkeiten zwischen Frontend und Backend getrennt sind, obwohl sie Teil desselben Projekts sind.

  • Frontend (src/app/page.tsx): Eine einzelne React-Seite, die den Verifizierungsablauf initiiert und das Ergebnis anzeigt. Sie interagiert mit der Digital Credential API des Browsers.
  • Backend API Routes (src/app/api/verify/...):
    • start/route.ts: Generiert die OpenID4VP-Anfrage und eine Sicherheits-Nonce.
    • finish/route.ts: Empfängt die Präsentation von der Wallet (über den Browser), validiert die Nonce und dekodiert den Nachweis.
  • Library (src/lib/):
    • database.ts: Verwaltet alle Datenbankinteraktionen (Erstellen von Challenges, Verifizieren von Sitzungen).
    • crypto.ts: Kümmert sich um die Dekodierung des CBOR-basierten mDoc-Nachweises.

Hier ist ein Diagramm, das die interne Architektur veranschaulicht:

4.3 Das Frontend bauen#

Unser Frontend ist bewusst schlank gehalten. Seine Hauptaufgabe ist es, als benutzerseitiger Auslöser für den Verifizierungsablauf zu dienen und sowohl mit unserem Backend als auch mit den nativen Funktionen des Browsers zur Handhabung von Nachweisen zu kommunizieren. Es enthält selbst keine komplexe Protokolllogik; das wird alles delegiert.

Konkret wird das Frontend Folgendes handhaben:

  • Benutzerinteraktion: Bietet eine einfache Oberfläche, wie einen „Verifizieren“-Button, damit der User den Prozess starten kann.
  • Zustandsverwaltung: Verwaltet den UI-Zustand, zeigt Ladeindikatoren während der Verifizierung an und präsentiert die endgültige Erfolgs- oder Fehlermeldung.
  • Backend-Kommunikation (Anfrage): Ruft /api/verify/start auf und empfängt eine strukturierte JSON-Payload (protocol, request, state), die genau beschreibt, was die Wallet präsentieren soll.
  • Aufruf der Browser-API: Übergibt dieses JSON-Objekt an navigator.credentials.get(), was einen nativen QR-Code rendert und auf die Antwort der Wallet wartet.
  • Backend-Kommunikation (Antwort): Sobald die Browser-API die Verifiable Presentation zurückgibt, sendet sie diese Daten zur endgültigen serverseitigen Validierung in einer POST-Anfrage an unseren /api/verify/finish-Endpunkt.
  • Ergebnisse anzeigen: Aktualisiert die UI, um den User darüber zu informieren, ob die Verifizierung erfolgreich war oder fehlgeschlagen ist, basierend auf der Antwort des Backends.

Die Kernlogik befindet sich in der startVerification-Funktion:

// src/app/page.tsx const startVerification = async () => { setLoading(true); setVerificationResult(null); try { // 1. Prüfen, ob der Browser die API unterstützt if (!navigator.credentials?.get) { throw new Error("Browser does not support the Credential API."); } // 2. Unser Backend nach einem Anfrageobjekt fragen const res = await fetch("/api/verify/start"); const { protocol, request } = await res.json(); // 3. Dieses Objekt an den Browser übergeben – dies löst den nativen QR-Code aus const credential = await (navigator.credentials as any).get({ mediation: "required", digital: { requests: [ { protocol, // "openid4vp" data: request, // enthält dcql_query, nonce, etc. }, ], }, }); // 4. Die Wallet-Antwort (vom Browser) zur serverseitigen Prüfung an unseren finish-Endpunkt weiterleiten const verifyRes = await fetch("/api/verify/finish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(credential), }); const result = await verifyRes.json(); if (verifyRes.ok && result.verified) { setVerificationResult(`Success: ${result.message}`); } else { throw new Error(result.message || "Verification failed."); } } catch (err) { setVerificationResult(`Error: ${(err as Error).message}`); } finally { setLoading(false); } };

Diese Funktion zeigt die vier entscheidenden Schritte der Frontend-Logik: Prüfung der API-Unterstützung, Abrufen der Anfrage vom Backend, Aufrufen der Browser-API und Zurücksenden des Ergebnisses zur Verifizierung. Der Rest der Datei ist Standard-React-Boilerplate für Zustands- und UI-Rendering, das du im GitHub-Repository einsehen kannst.

Warum digital und mediation: 'required'?#

Du könntest bemerken, dass unser Aufruf von navigator.credentials.get() anders aussieht als in einfacheren Beispielen. Das liegt daran, dass wir uns strikt an die offizielle W3C Digital Credentials API-Spezifikation halten.

  • digital-Member: Die Spezifikation verlangt, dass alle Anfragen für digitale Nachweise in einem digital-Objekt verschachtelt sind. Dies bietet einen klaren, standardisierten Namensraum für diese API, unterscheidet sie von anderen Nachweistypen (wie password oder federated) und ermöglicht zukünftige Erweiterungen ohne Konflikte.

  • mediation: 'required': Diese Option ist ein entscheidendes Sicherheits- und Benutzererlebnis-Feature. Sie erzwingt, dass der User aktiv mit einer Aufforderung interagieren muss (z. B. einem biometrischen Scan, einer PIN-Eingabe oder einem Zustimmungsbildschirm), um die Nachweisanfrage zu genehmigen. Ohne diese Option könnte eine Website potenziell versuchen, im Hintergrund unbemerkt auf Nachweise zuzugreifen, was ein erhebliches Datenschutzrisiko darstellt. Indem wir Mediation erfordern, stellen wir sicher, dass der User immer die Kontrolle behält und für jede Transaktion seine ausdrückliche Zustimmung gibt.

4.4 Die Backend-Endpunkte bauen#

Mit der React-UI an Ort und Stelle benötigen wir nun zwei API-Routen, die die schwere Arbeit auf dem Server erledigen:

  1. /api/verify/start – erstellt eine OpenID4VP-Anfrage, speichert eine einmalige Challenge in MySQL und gibt alles an den Browser zurück.
  2. /api/verify/finish – empfängt die Wallet-Antwort, validiert die Challenge, verifiziert & dekodiert den Nachweis und gibt schließlich ein prägnantes JSON-Ergebnis an die UI zurück.

4.4.1 /api/verify/start: Die OpenID4VP-Anfrage generieren#

// src/app/api/verify/start/route.ts import { NextResponse } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { createChallenge, cleanupExpiredChallenges } from "@/lib/database"; export async function GET() { // 1️⃣ Eine kurzlebige, zufällige Nonce (Challenge) erstellen const challenge = uuidv4(); const challengeId = uuidv4(); const expiresAt = new Date(Date.now() + 5 * 60 * 1000); await createChallenge(challengeId, challenge, expiresAt); cleanupExpiredChallenges().catch(console.error); // 2️⃣ Eine DCQL-Abfrage erstellen, die beschreibt, *was* wir wollen const dcqlQuery = { credentials: [ { id: "cred1", format: "mso_mdoc", meta: { doctype_value: "eu.europa.ec.eudi.pid.1" }, claims: [ { path: ["eu.europa.ec.eudi.pid.1", "family_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "given_name"] }, { path: ["eu.europa.ec.eudi.pid.1", "birth_date"] }, ], }, ], }; // 3️⃣ Ein Objekt zurückgeben, das der Browser an navigator.credentials.get() übergeben kann return NextResponse.json({ protocol: "openid4vp", // teilt dem Browser mit, welches Wallet-Protokoll zu verwenden ist request: { dcql_query: dcqlQuery, // WAS zu präsentieren ist nonce: challenge, // Schutz vor Wiederholungsangriffen response_type: "vp_token", response_mode: "dc_api", // die Wallet wird direkt an /finish POSTen }, state: { credential_type: "mso_mdoc", // für spätere Prüfungen aufbewahrt nonce: challenge, challenge_id: challengeId, }, }); }

Schlüsselparameter

nonce – eine kryptografische Challenge, die Anfrage & Antwort verbindet (verhindert Replay-Angriffe). • dcql_query – Ein Objekt, das die genauen Claims beschreibt, die wir benötigen. Für diesen Guide verwenden wir eine dcql_query-Struktur, die von neueren Entwürfen der Digital Credential Query Language inspiriert ist, auch wenn dies noch kein finalisierter Standard ist. • state – beliebiges JSON, das von der Wallet zurückgespiegelt wird, damit wir den DB-Eintrag nachschlagen können.

4.4.2 Datenbank-Helfer#

Die Datei src/lib/database.ts kapselt die grundlegenden MySQL-Operationen für Challenges & Verifizierungssitzungen (einfügen, lesen, als verwendet markieren). Diese Logik in einem einzigen Modul zu halten, macht es einfach, den Datenspeicher später auszutauschen.


4.5 /api/verify/finish: Die Präsentation validieren & dekodieren#

// src/app/api/verify/finish/route.ts import { NextResponse, NextRequest } from "next/server"; import { v4 as uuidv4 } from "uuid"; import { getChallenge, markChallengeAsUsed, createVerificationSession, updateVerificationSession, } from "@/lib/database"; import { decodeDigitalCredential, decodeAllNamespaces } from "@/lib/crypto"; export async function POST(request: NextRequest) { const body = await request.json(); // 1️⃣ Die Teile der verifizierbaren Präsentation extrahieren const vpTokenMap = body.vp_token ?? body.data?.vp_token; const state = body.state; const mdocToken = vpTokenMap?.cred1; // nach dieser ID haben wir in dcqlQuery gefragt if (!vpTokenMap || !state || !mdocToken) { return NextResponse.json( { verified: false, message: "Malformed response" }, { status: 400 }, ); } // 2️⃣ Einmalige Challenge-Validierung const stored = await getChallenge(state.nonce); if (!stored) { return NextResponse.json( { verified: false, message: "Invalid or expired challenge" }, { status: 400 }, ); } const sessionId = uuidv4(); await createVerificationSession(sessionId, stored.id); // 3️⃣ (Pseudo) kryptografische Prüfungen – in der Produktion durch echte mDL-Validierung ersetzen // In einer echten Anwendung würdest du eine dedizierte Bibliothek verwenden, um eine vollständige // kryptografische Validierung der mdoc-Signatur gegen den öffentlichen Schlüssel des Ausstellers durchzuführen. const isValid = mdocToken.length > 0; if (!isValid) { await updateVerificationSession(sessionId, "failed", { reason: "mdoc validation failed", }); return NextResponse.json( { verified: false, message: "Credential validation failed" }, { status: 400 }, ); } // 4️⃣ Die mobile-DL (mdoc) Payload in lesbares JSON dekodieren const decoded = await decodeDigitalCredential(mdocToken); const readable = decodeAllNamespaces(decoded)["eu.europa.ec.eudi.pid.1"]; await markChallengeAsUsed(state.nonce); await updateVerificationSession(sessionId, "verified", { readable }); return NextResponse.json({ verified: true, message: "mdoc credential verified successfully!", credentialData: readable, sessionId, }); }

Wichtige Felder in der Wallet-Antwort

vp_token – Map, die jeden Nachweis enthält, den die Wallet zurückgibt. Für unsere Demo holen wir vp_token.cred1. • state – Echo des Blobs, den wir in /start bereitgestellt haben; enthält die nonce, damit wir den DB-Eintrag nachschlagen können. • mdocToken – eine Base64URL-kodierte CBOR-Struktur, die das ISO mDoc darstellt.

4.6 Den mdoc-Nachweis dekodieren#

Wenn der Verifier einen mdoc-Nachweis vom Browser erhält, handelt es sich um einen Base64URL-String, der CBOR-kodierte Binärdaten enthält. Um die eigentlichen Claims zu extrahieren, führt der finish-Endpunkt einen mehrstufigen Dekodierungsprozess mit Hilfsfunktionen aus src/lib/crypto.ts durch.

4.6.1 Schritt 1: Base64URL- und CBOR-Dekodierung#

Die Funktion decodeDigitalCredential kümmert sich um die Umwandlung des kodierten Strings in ein nutzbares Objekt:

// src/lib/crypto.ts export async function decodeDigitalCredential(encodedCredential: string) { // 1. Base64URL in Standard-Base64 umwandeln const base64UrlToBase64 = (input: string) => { let base64 = input.replace(/-/g, "+").replace(/_/g, "/"); const pad = base64.length % 4; if (pad) base64 += "=".repeat(4 - pad); return base64; }; const base64 = base64UrlToBase64(encodedCredential); // 2. Base64 in Binärdaten dekodieren const binaryString = atob(base64); const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); // 3. CBOR dekodieren const decoded = await cbor.decodeFirst(byteArray); return decoded; }
  • Base64URL zu Base64: Wandelt den Nachweis von Base64URL- in Standard-Base64-Kodierung um.
  • Base64 zu Binär: Dekodiert den Base64-String in ein binäres Byte-Array.
  • CBOR-Dekodierung: Verwendet die cbor-web-Bibliothek, um die Binärdaten in ein strukturiertes JavaScript-Objekt zu dekodieren.

4.6.2 Schritt 2: Namespaced Claims extrahieren#

Die Funktion decodeAllNamespaces verarbeitet das dekodierte CBOR-Objekt weiter, um die eigentlichen Claims aus den relevanten Namespaces zu extrahieren:

// src/lib/crypto.ts export function decodeAllNamespaces(jsonObj) { const decoded = {}; try { jsonObj.documents.forEach((doc, idx) => { // 1) issuerSigned.nameSpaces: const issuerNS = doc.issuerSigned?.nameSpaces || {}; Object.entries(issuerNS).forEach(([nsName, entries]) => { if (!decoded[nsName]) decoded[nsName] = {}; (entries as any[]).forEach((entry) => { const bytes = Uint8Array.from(entry.value); const decodedEntry = cbor.decodeFirstSync(bytes); Object.assign(decoded[nsName], decodedEntry); }); }); // 2) deviceSigned.nameSpaces (falls vorhanden): const deviceNS = doc.deviceSigned?.nameSpaces; if (deviceNS?.value?.data) { const bytes = Uint8Array.from(deviceNS.value); decoded[`deviceSigned_ns_${idx}`] = cbor.decodeFirstSync(bytes); } }); } catch (e) { console.error(e); } return decoded; }
  • Iteriert über alle Dokumente im dekodierten Nachweis.
  • Dekodiert jeden Namespace (z. B. eu.europa.ec.eudi.pid.1), um die tatsächlichen Claim-Werte (wie Name, Geburtsdatum usw.) zu extrahieren.
  • Behandelt sowohl vom Aussteller signierte als auch vom Gerät signierte Namespaces, falls vorhanden.

Beispiel-Ausgabe#

Nachdem diese Schritte durchlaufen wurden, erhält der finish-Endpunkt ein menschenlesbares Objekt, das die Claims aus dem mdoc enthält, zum Beispiel:

{ "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" }

Dieser Prozess stellt sicher, dass der Verifier die notwendigen Informationen aus dem mdoc-Nachweis sicher und zuverlässig zur Anzeige und weiteren Verarbeitung extrahieren kann.

4.7 Das Ergebnis in der UI anzeigen#

Der finish-Endpunkt gibt ein minimales JSON-Objekt an das Frontend zurück:

{ "verified": true, "message": "mdoc credential verified successfully!", "credentialData": { "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" } }

Das Frontend empfängt diese Antwort in startVerification() und speichert sie einfach im React-Zustand, sodass wir eine schöne Bestätigungskarte rendern oder einzelne Claims anzeigen können – z. B. „Willkommen, John Doe (geboren 1990-01-01)!“.

5. Den Verifier ausführen und nächste Schritte#

Du hast jetzt einen vollständigen, funktionierenden Verifier, der die nativen Funktionen des Browsers zur Handhabung von Nachweisen nutzt. Hier erfährst du, wie du ihn lokal ausführen kannst und was du tun kannst, um ihn von einem Proof-of-Concept zu einer produktionsreifen Anwendung zu machen.

5.1 Wie man das Beispiel ausführt#

  1. Repository klonen:

    git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
  2. Abhängigkeiten installieren:

    npm install
  3. Datenbank starten: Stelle sicher, dass Docker auf deinem Rechner läuft, und starte dann den MySQL-Container:

    docker-compose up -d
  4. Anwendung ausführen:

    npm run dev

    Öffne deinen Browser unter http://localhost:3000, und du solltest die Benutzeroberfläche des Verifiers sehen. Du kannst nun deine CMWallet verwenden, um den QR-Code zu scannen und den Verifizierungsprozess abzuschließen.

5.2 Nächste Schritte: Vom Demo zur Produktion#

Dieses Tutorial liefert die grundlegenden Bausteine für einen Verifier. Um ihn produktionsreif zu machen, müsstest du mehrere zusätzliche Funktionen implementieren:

  • Vollständige kryptografische Validierung: Die aktuelle Implementierung verwendet eine Platzhalterprüfung (mdocToken.length > 0). In einem realen Szenario musst du eine vollständige kryptografische Validierung der mdoc-Signatur gegen den öffentlichen Schlüssel des Ausstellers durchführen (z. B. durch Auflösung seiner DID oder Abrufen seines Public-Key-Zertifikats). Für Standards zur DID-Auflösung siehe die W3C DID Resolution-Spezifikation.

  • Überprüfung des Widerrufsstatus durch den Aussteller: Nachweise können vom Aussteller vor ihrem Ablaufdatum widerrufen werden. Ein Produktions-Verifier muss den Status des Nachweises überprüfen, indem er eine Widerrufsliste oder einen Status-Endpunkt des Ausstellers abfragt. Die W3C Verifiable Credentials Status List bietet den Standard für Widerrufslisten von Nachweisen.

  • Robuste Fehlerbehandlung & Sicherheit: Füge eine umfassende Fehlerbehandlung, Eingabevalidierung und Ratenbegrenzung für API-Endpunkte hinzu und stelle sicher, dass die gesamte Kommunikation über HTTPS (TLS) erfolgt, um Daten während der Übertragung zu schützen. Die OWASP API Security Guidelines bieten umfassende Best Practices für die API-Sicherheit.

  • Unterstützung für mehrere Nachweistypen: Erweitere die Logik, um verschiedene doctype-Werte und Nachweisformate zu handhaben, wenn du erwartest, mehr als nur den europäischen Digital Identity (EUDI) PID-Nachweis zu erhalten. Das W3C Verifiable Credentials Data Model bietet umfassende Spezifikationen für VC-Formate.

5.3 Was nicht im Rahmen dieses Tutorials liegt#

Dieses Beispiel konzentriert sich bewusst auf den zentralen browservermittelten Ablauf, um es leicht verständlich zu machen. Die folgenden Themen werden als außerhalb des Rahmens betrachtet:

  • Produktionsreife Sicherheit: Der Verifier dient zu Bildungszwecken und verfügt nicht über die für eine Live-Umgebung erforderliche Härtung.
  • W3C Verifiable Credentials: Dieses Tutorial konzentriert sich ausschließlich auf das ISO mDoc-Format für mobile Führerscheine. Es behandelt keine anderen gängigen Formate wie JWT-VCs oder VCs mit Linked Data Proofs (LD-Proofs).
  • Fortgeschrittene OpenID4VP-Flows: Wir implementieren keine komplexeren OpenID4VP-Funktionen wie die direkte Kommunikation zwischen Wallet und Backend über eine redirect_uri oder die dynamische Client-Registrierung.

Indem du auf diesem Fundament aufbaust und diese nächsten Schritte einbeziehst, kannst du einen robusten und sicheren Verifier entwickeln, der in der Lage ist, digitale Nachweise in deinen eigenen Anwendungen zu vertrauen und zu validieren.

Fazit#

Das war's! Mit weniger als 250 Zeilen TypeScript haben wir jetzt einen End-to-End-Verifier, der:

  1. Eine Anfrage für die Credential-API des Browsers veröffentlicht.
  2. Jeder konformen Wallet erlaubt, eine Verifiable Presentation bereitzustellen.
  3. Die Präsentation auf dem Server validiert.
  4. Die Benutzeroberfläche in Echtzeit aktualisiert.

In der Produktion würdest du die Platzhalter-Validierung durch vollständige ISO 18013-5-Prüfungen ersetzen, Widerrufsabfragen beim Aussteller hinzufügen, Ratenbegrenzung, Audit-Logging und natürlich End-to-End-TLS – aber die grundlegenden Bausteine bleiben genau dieselben.

Ressourcen#

Hier sind einige der wichtigsten Ressourcen, Spezifikationen und Werkzeuge, die in diesem Tutorial verwendet oder referenziert wurden:

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start Free Trial

Share this article


LinkedInTwitterFacebook