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
See the original blog version in English here.
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.
Recent Articles
📝
So erstellst du einen Verifier für digitale Nachweise (Entwickler-Guide)
📝
Wie man einen Issuer für digitale Nachweise erstellt (Entwickler-Guide)
📖
WebAuthn Resident Keys: Discoverable Credentials als Passkeys
🔑
Physischer Zutritt per Badge & Passkeys: Ein technischer Guide
🔑
MFA-Pflicht & der Umstieg auf Passkeys: Best Practices
Die Magie hinter digitalen Nachweisen liegt in einem einfachen, aber leistungsstarken „Vertrauensdreieck“-Modell, das drei Hauptakteure umfasst:
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.
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.
Bevor du beginnst, stelle sicher, dass du Folgendes hast:
Wir werden nun jede dieser Voraussetzungen im Detail durchgehen, beginnend mit den Standards und Protokollen, die diesem mdoc-basierten Verifier zugrunde liegen.
Unser Verifier ist für Folgendes ausgelegt:
Standard / Protokoll | Beschreibung |
---|---|
W3C VC | Das W3C Verifiable Credentials Data Model. Es definiert die Standardstruktur für digitale Nachweise, einschließlich Claims, Metadaten und Proofs. |
SD-JWT | Selective 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 mDoc | ISO/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. |
OpenID4VP | OpenID 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:
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.
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.
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:
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.
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.
Next.js ist unser Framework der Wahl, da es eine nahtlose, integrierte Erfahrung für die Erstellung von Full-Stack-Anwendungen bietet.
redirect_uri
zu fungieren, um die endgültige
Antwort von der CMWallet sicher zu empfangen und zu verifizieren.Unsere Implementierung stützt sich auf eine spezifische Reihe von Bibliotheken für Frontend und Backend:
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.
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):
Hinweis: Installiere APK-Dateien nur aus Quellen, denen du vertraust. Der bereitgestellte Link stammt aus dem offiziellen Projekt-Repository.
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.
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:
Digitale Signaturen werden mittels Public-Key-Kryptografie (auch asymmetrische Kryptografie genannt) erstellt. So funktioniert es in unserem Kontext:
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.
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:
alg
).vc
-Claim), einschließlich
des issuer
, credentialSubject
usw.// 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.
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:
Unser Verifier muss dann zwei separate Signaturprüfungen durchführen:
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.
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.
navigator.credentials.get()
API des Browsers zu übergeben, das Ergebnis zu empfangen
und es zur Verifizierung an unser Backend weiterzuleiten.openid4vp
-Protokoll und generiert nativ einen QR-Code. Dann
wartet er auf die Antwort der Wallet.Hier ist ein Sequenzdiagramm, das den vollständigen und korrekten Ablauf veranschaulicht:
Der Ablauf erklärt:
/api/verify/start
) auf, das
ein Anfrageobjekt mit der Abfrage und einer Nonce generiert und zurückgibt.navigator.credentials.get()
mit dem
Anfrageobjekt auf.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“.
.get()
-Promise im Frontend wird schließlich aufgelöst und liefert die
Präsentations-Payload./api/verify/finish
-Endpunkt unseres Backends. Das Backend validiert die Nonce
und den Nachweis.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
Zuerst initialisieren wir ein neues Next.js-Projekt, installieren die notwendigen Abhängigkeiten und starten unsere Datenbank.
Ö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.
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.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.
Unsere Next.js-Anwendung ist so strukturiert, dass die Zuständigkeiten zwischen Frontend und Backend getrennt sind, obwohl sie Teil desselben Projekts sind.
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.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.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:
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:
/api/verify/start
auf und empfängt eine
strukturierte JSON-Payload (protocol
, request
, state
), die genau beschreibt, was
die Wallet präsentieren soll.navigator.credentials.get()
, was einen nativen QR-Code rendert und auf die Antwort der
Wallet wartet./api/verify/finish
-Endpunkt.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.
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.
Mit der React-UI an Ort und Stelle benötigen wir nun zwei API-Routen, die die schwere Arbeit auf dem Server erledigen:
/api/verify/start
– erstellt eine OpenID4VP-Anfrage, speichert eine einmalige
Challenge in MySQL und gibt alles an den Browser zurück./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./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.
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.
/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.
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.
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; }
cbor-web
-Bibliothek, um die Binärdaten in ein
strukturiertes JavaScript-Objekt zu dekodieren.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; }
eu.europa.ec.eudi.pid.1
), um die tatsächlichen
Claim-Werte (wie Name, Geburtsdatum usw.) zu extrahieren.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.
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)!“.
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.
Repository klonen:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
Abhängigkeiten installieren:
npm install
Datenbank starten: Stelle sicher, dass Docker auf deinem Rechner läuft, und starte dann den MySQL-Container:
docker-compose up -d
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.
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.
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:
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.
Das war's! Mit weniger als 250 Zeilen TypeScript haben wir jetzt einen End-to-End-Verifier, der:
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.
Hier sind einige der wichtigsten Ressourcen, Spezifikationen und Werkzeuge, die in diesem Tutorial verwendet oder referenziert wurden:
Projekt-Repository:
Wichtige Spezifikationen:
Werkzeuge:
Bibliotheken:
Related Articles
Table of Contents