Next.js、OpenID4VP、ISO mDocを使用して、デジタルクレデンシャルVerifierをゼロから構築する方法を学びましょう。このステップバイステップの開発者ガイドでは、モバイル運転免許証やその他のデジタルクレデンシャルを要求、受信、検証できるVerifierの作成方法を解説します。
Amine
Created: August 20, 2025
Updated: August 21, 2025
See the original blog version in English here.
オンラインでの本人確認は常に課題であり、パスワードへの依存や、安全でないチャネルでの機密文書の共有につながっています。これにより、企業にとって本人確認は、時間とコストがかかり、不正行為が発生しやすいプロセスとなっていました。デジタルクレデンシャルは、ユーザーが自分のデータを再び管理できるようにする、新しいアプローチを提供します。これらは物理的なwalletのデジタル版であり、運転免許証から大学の学位まであらゆるものを格納できますが、暗号学的に安全で、プライバシーを保護し、即座に検証可能であるという付加的な利点があります。
このガイドでは、開発者向けにデジタルクレデンシャルのVerifierを構築するための実践的なステップバイステップのチュートリアルを提供します。標準は存在しますが、その実装に関するガイダンスはほとんどありません。このチュートリアルではそのギャップを埋め、ブラウザネイティブのDigital Credential API、プレゼンテーションプロトコルとしてのOpenID4VP、クレデンシャルフォーマットとしてのISO mDoc(例:モバイル運転免許証)を使用してVerifierを構築する方法を紹介します。
最終的には、互換性のあるモバイルwalletからデジタルクレデンシャルを要求、受信、検証できる、シンプルかつ機能的なNext.jsアプリケーションが完成します。
完成したアプリケーションの動作を簡単に見てみましょう。プロセスは主に4つのステップで構成されます。
ステップ1:初期ページ ユーザーは初期ページにアクセスし、「Digital Identityで検証」をクリックしてプロセスを開始します。
ステップ2:信頼プロンプト ブラウザはユーザーに信頼の確認を求めます。ユーザーは「続行」をクリックして次に進みます。
ステップ3:QRコードのスキャン QRコードが表示され、ユーザーは互換性のあるwalletアプリケーションでスキャンします。
ステップ4:デコードされたクレデンシャル 検証が成功すると、アプリケーションはデコードされたクレデンシャルデータを表示します。
デジタルクレデンシャルの背後にある仕組みは、3つの主要な役割が関わるシンプルかつ強力な「信頼の三角形」モデルにあります。
ユーザーがサービスにアクセスしたい場合、自分のwalletからクレデンシャルを提示します。Verifierは、元のIssuerに直接問い合わせることなく、その真正性を即座に確認できます。
この分散型アイデンティティエコシステムが発展するためには、Verifierの役割が絶対に不可欠です。彼らはこの新しい信頼インフラストラクチャのゲートキーパーであり、クレデンシャルを利用して実世界で役立つものにする存在です。下の図が示すように、VerifierはHolderからクレデンシャルを要求、受信、検証することで信頼の三角形を完成させます。
開発者であれば、この検証を実行するサービスを構築することは、次世代の安全でユーザー中心のアプリケーションにとって基本的なスキルです。このガイドは、まさにそのプロセスを順を追って説明するために設計されています。中核となる概念や標準から、署名の検証やクレデンシャルの状態確認といった実装の詳細まで、あなた自身の検証可能なクレデンシャルVerifierを構築するために知っておくべきすべてをカバーします。
先に進みたいですか? このチュートリアルの完成版プロジェクトはGitHubで公開しています。自由にクローンして試してみてください: https://github.com/corbado/digital-credentials-example
さあ、始めましょう。
始める前に、以下の準備ができていることを確認してください。
これから、これらの前提条件を一つずつ詳しく見ていき、mdocベースのVerifierを支える標準とプロトコルから始めます。
私たちのVerifierは、以下に基づいて構築されています。
標準 / プロトコル | 説明 |
---|---|
W3C VC | W3C Verifiable Credentialsデータモデル。クレーム、メタデータ、証明を含むデジタルクレデンシャルの標準構造を定義します。 |
SD-JWT | JWTの選択的開示。JSON Web TokenをベースにしたVCのフォーマットで、保有者がクレデンシャルから特定のクレームのみを選択的に開示できるようにし、プライバシーを向上させます。 |
ISO mDoc | ISO/IEC 18013-5。モバイル運転免許証(mDL)やその他のモバイルIDの国際標準で、オフラインおよびオンラインでの使用のためのデータ構造と通信プロトコルを定義します。 |
OpenID4VP | OpenID for Verifiable Presentations。OAuth 2.0上に構築された相互運用可能なプレゼンテーションプロトコル。Verifierがクレデンシャルを要求し、保有者のwalletがそれを提示する方法を定義します。 |
このチュートリアルでは、具体的に以下を使用します。
スコープに関する注意: より広い文脈を提供するためにW3C VCとSD-JWTを簡単に紹介しますが、このチュートリアルではOpenID4VPを介したISO mDocクレデンシャルの実装に限定しています。W3CベースのVCはこの例のスコープ外です。
ISO/IEC 18013-5 mDoc標準は、モバイル運転免許証(mDL)などのモバイルドキュメントの構造とエンコーディングを定義します。mDocクレデンシャルはCBORでエンコードされ、暗号学的に署名されており、検証のためにデジタルで提示できます。私たちのVerifierは、これらのmdocクレデンシャルのデコードと検証に焦点を当てます。
OpenID4VPは、OAuth 2.0とOpenID Connectの上に構築された、デジタルクレデンシャルを要求および提示するための相互運用可能なプロトコルです。この実装では、OpenID4VPは以下の目的で使用されます。
標準とプロトコルを明確に理解したところで、次はVerifierを構築するための適切な技術スタックを選択する必要があります。私たちの選択は、堅牢性、開発者体験、そして現代のウェブエコシステムとの互換性を考慮して設計されています。
フロントエンドとバックエンドの両方のコードにTypeScriptを使用します。JavaScriptのスーパーセットとして、静的型付けを追加することで、エラーを早期に発見し、コードの品質を向上させ、複雑なアプリケーションの管理を容易にします。クレデンシャル検証のようなセキュリティに敏感な文脈では、型安全性は大きな利点です。
Next.jsは、フルスタックアプリケーションを構築するためのシームレスで統合された体験を提供するため、私たちが選択したフレームワークです。
redirect_uri
として機能する責任を負います。私たちの実装は、フロントエンドとバックエンドで特定のライブラリセットに依存しています。
openid-client
に関する注意:
より高度な本番グレードのVerifierは、バックエンドでOpenID4VPプロトコルを直接処理するためにopenid-client
ライブラリを使用するかもしれません。これにより、動的なredirect_uri
などの機能が可能になります。redirect_uri
を持つサーバー駆動のOpenID4VPフローでは、openid-client
はvp_token
レスポンスを直接解析および検証するために使用されます。このチュートリアルでは、よりシンプルでブラウザを介したフローを使用しているため、これは不要であり、プロセスが理解しやすくなっています。
この技術スタックにより、ブラウザのDigital Credential APIとISO mDocクレデンシャルフォーマットに焦点を当てた、堅牢で型安全、かつスケーラブルなVerifierの実装が保証されます。
Verifierをテストするには、ブラウザのDigital Credential APIと対話できるモバイルwalletが必要です。
私たちは、Android用の堅牢なOpenID4VP準拠のテストwalletである**CMWallet**を使用します。
CMWalletのインストール方法(Android):
注意: 信頼できるソースからのAPKファイルのみをインストールしてください。提供されているリンクは公式プロジェクトリポジトリのものです。
実装に入る前に、検証可能なクレデンシャルを支える暗号技術の概念を理解することが不可欠です。これが、クレデンシャルを「検証可能」で信頼できるものにしているのです。
Verifiable Credentialの核心は、Issuerによってデジタル署名された一連のクレーム(名前、生年月日など)です。デジタル署名は、2つの重要な保証を提供します。
デジタル署名は、公開鍵/秘密鍵暗号方式(非対称暗号とも呼ばれる)を使用して作成されます。私たちの文脈では、次のように機能します。
DIDに関する注意: このチュートリアルでは、DIDを介してIssuerの鍵を解決しません。本番環境では、通常、IssuerはDIDやその他の信頼できるエンドポイントを介して公開鍵を公開し、Verifierはそれを暗号検証に使用します。
検証可能なクレデンシャルは、しばしば**JSON Web Tokens
(JWTs)**としてフォーマットされます。JWTは、二者間で転送されるクレームを表現するためのコンパクトでURLセーフな方法です。署名付きJWT(JWSとも呼ばれる)は、ドット(.
)で区切られた3つの部分から構成されます。
alg
)など、トークンに関するメタデータを含みます。issuer
、credentialSubject
などを含む、Verifiable Credentialの実際のクレーム(vc
クレーム)を含みます。// JWT構造の例 [ヘッダー].[ペイロード].[署名]
注意: JWTベースの検証可能なクレデンシャルは、このブログ記事のスコープ外です。この実装は、ISO mDocクレデンシャルとOpenID4VPに焦点を当てており、W3C Verifiable CredentialsやJWTベースのクレデンシャルは対象外です。
Verifierにとって、クレデンシャルが有効であることだけを知るだけでは不十分です。クレデンシャルを提示している人物が正当な保有者であることも知る必要があります。これにより、誰かが盗まれたクレデンシャルを使用するのを防ぎます。
これは**Verifiable Presentation (VP)**を使用して解決されます。VPは、1つ以上のVCを包むラッパーであり、保有者自身によって署名されています。
フローは次のとおりです。
私たちのVerifierは、次に2つの別々の署名チェックを実行する必要があります。
この2段階のチェックにより、クレデンシャルの真正性とそれを提示している人物のアイデンティティの両方が保証され、堅牢で安全な信頼モデルが構築されます。
注意: W3C VCエコシステムで定義されているVerifiable
Presentationsの概念は、このブログ記事のスコープ外です。ここでのVerifiable Presentationという用語は、OpenID4VPのvp_token
レスポンスを指し、これはW3C
VPと似た動作をしますが、W3CのJSON-LD署名モデルではなくISO
mDocのセマンティクスに基づいています。このガイドはISO
mDocクレデンシャルとOpenID4VPに焦点を当てており、W3C Verifiable
Presentationsやその署名検証は対象外です。
私たちのVerifierアーキテクチャは、ブラウザに組み込まれたDigital Credential APIを安全な仲介役として使用し、WebアプリケーションとユーザーのモバイルCMWalletを接続します。このアプローチは、ブラウザにネイティブのQRコード表示とwallet通信を処理させることで、フローを簡素化します。
navigator.credentials.get()
APIに渡し、結果を受け取って検証のためにバックエンドに転送するのが仕事です。openid4vp
プロトコルを理解し、ネイティブにQRコードを生成します。その後、walletがレスポンスを返すのを待ちます。以下は、完全かつ正確なフローを示すシーケンス図です。
フローの説明:
/api/verify/start
)を呼び出し、バックエンドはクエリとnonceを含むリクエストオブジェクトを生成して返します。navigator.credentials.get()
を呼び出します。openid4vp
プロトコルリクエストを認識し、ネイティブにQRコードを表示します。.get()
のpromiseは保留状態になります。注意:
このQRコードフローはデスクトップブラウザで発生します。モバイルブラウザ(実験的フラグを有効にしたAndroid
Chrome)では、ブラウザは同じデバイス上の互換性のあるwalletと直接通信できるため、QRコードのスキャンは不要です。Android
Chromeでこの機能を有効にするには、chrome://flags#web-identity-digital-credentials
に移動し、フラグを「Enabled」に設定してください。
.get()
promiseが最終的に解決され、プレゼンテーションのペイロードが渡されます。/api/verify/finish
エンドポイントにPOSTします。バックエンドはnonceとクレデンシャルを検証します。標準、プロトコル、アーキテクチャフローについてしっかりと理解できたので、Verifierの構築を開始できます。
一緒に進めるか、完成版コードを使用するか
これから、セットアップとコード実装をステップバイステップで進めていきます。もし完成品に直接ジャンプしたい場合は、GitHubリポジトリから完全なプロジェクトをクローンしてローカルで実行できます。
git clone https://github.com/corbado/digital-credentials-example.git
まず、新しいNext.jsプロジェクトを初期化し、必要な依存関係をインストールし、データベースを起動します。
ターミナルを開き、プロジェクトを作成したいディレクトリに移動して、次のコマンドを実行します。このプロジェクトではApp Router、TypeScript、Tailwind CSSを使用します。
npx create-next-app@latest . --ts --eslint --tailwind --app --src-dir --import-alias "@/*" --use-npm
このコマンドは、現在のディレクトリに新しいNext.jsアプリケーションの雛形を作成します。
次に、CBORデコーディング、データベース接続、UUID生成を処理するライブラリをインストールする必要があります。
npm install cbor-web mysql2 uuid @types/uuid
このコマンドは以下をインストールします。
cbor-web
: mdocクレデンシャルペイロードをデコードするため。mysql2
: データベース用のMySQLクライアント。uuid
: 一意のチャレンジ文字列を生成するため。@types/uuid
: uuid
ライブラリ用のTypeScript型定義。私たちのバックエンドは、OIDCセッションデータを保存するためにMySQLデータベースを必要とします。これにより、各検証フローが安全でステートフルであることが保証されます。これを簡単にするためにdocker-compose.yml
ファイルを含めました。
リポジトリをクローンした場合は、単にdocker-compose up -d
を実行するだけです。ゼロから構築している場合は、docker-compose.yml
という名前のファイルに以下の内容で作成してください。
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:
このDocker
Compose設定には、SQL初期化スクリプトも必要です。sql
という名前のディレクトリを作成し、その中にinit.sql
という名前のファイルに必要なテーブルをセットアップするために以下の内容で作成します。
-- 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) );
両方のファイルが配置されたら、プロジェクトのルートでターミナルを開き、次を実行します。
docker-compose up -d
このコマンドは、バックグラウンドでMySQLコンテナを起動します。
私たちのNext.jsアプリケーションは、同じプロジェクトの一部でありながら、フロントエンドとバックエンドの関心事を分離するように構成されています。
src/app/page.tsx
):
検証フローを開始し、結果を表示する単一のReactページ。ブラウザのDigital
Credential APIと対話します。src/app/api/verify/...
):
start/route.ts
: OpenID4VPリクエストとセキュリティnonceを生成します。finish/route.ts
:
walletからのプレゼンテーション(ブラウザ経由)を受信し、nonceを検証し、クレデンシャルをデコードします。src/lib/
):
database.ts
: すべてのデータベース操作(チャレンジの作成、セッションの検証)を管理します。crypto.ts
: CBORベースのmDocクレデンシャルのデコードを処理します。以下は、内部アーキテクチャを示す図です。
私たちのフロントエンドは意図的に軽量です。その主な責任は、検証フローのユーザー向けトリガーとして機能し、バックエンドとブラウザのネイティブなクレデンシャル処理機能の両方と通信することです。それ自体には複雑なプロトコルロジックは含まれていません。それはすべて委任されています。
具体的には、フロントエンドは以下を処理します。
/api/verify/start
を呼び出し、walletが何を提示すべきかを正確に記述した構造化されたJSONペイロード(protocol
、request
、state
)を受け取ります。navigator.credentials.get()
に渡し、ネイティブのQRコードをレンダリングしてwalletの応答を待ちます。/api/verify/finish
エンドポイントに送信し、最終的なサーバーサイドの検証を行います。中心となるロジックはstartVerification
関数にあります。
// src/app/page.tsx const startVerification = async () => { setLoading(true); setVerificationResult(null); try { // 1. ブラウザがAPIをサポートしているか確認 if (!navigator.credentials?.get) { throw new Error("Browser does not support the Credential API."); } // 2. バックエンドにリクエストオブジェクトを要求 const res = await fetch("/api/verify/start"); const { protocol, request } = await res.json(); // 3. そのオブジェクトをブラウザに渡す – これがネイティブQRコードをトリガーする const credential = await (navigator.credentials as any).get({ mediation: "required", digital: { requests: [ { protocol, // "openid4vp" data: request, // contains dcql_query, nonce, etc. }, ], }, }); // 4. walletの応答(ブラウザから)をサーバーサイドのチェックのためにfinishエンドポイントに転送 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); } };
この関数は、APIサポートのチェック、バックエンドからのリクエスト取得、ブラウザAPIの呼び出し、そして検証のための結果の送り返しという、フロントエンドロジックの4つの主要なステップを示しています。ファイルの残りの部分は、状態とUIレンダリングのための標準的なReactの定型コードで、GitHubリポジトリで確認できます。
digital
とmediation: 'required'
なのか?#navigator.credentials.get()
の呼び出しが、より単純な例とは異なって見えることに気づくかもしれません。これは、私たちが公式のW3C Digital Credentials API仕様に厳密に従っているためです。
digital
メンバー:
仕様では、すべてのデジタルクレデンシャルリクエストをdigital
オブジェクト内にネストすることが要求されています。これにより、このAPIに明確で標準化された名前空間が提供され、他のクレデンシャルタイプ(password
やfederated
など)と区別し、競合することなく将来の拡張が可能になります。
mediation: 'required'
:
このオプションは、セキュリティとユーザーエクスペリエンスにとって重要な機能です。ユーザーがクレデンシャルリクエストを承認するために、プロンプト(生体認証スキャン、PIN入力、同意画面など)と能動的に対話することを強制します。これがないと、ウェブサイトがバックグラウンドで静かにクレデンシャルにアクセスしようとする可能性があり、重大なプライバシーリスクとなります。仲介を要求することで、ユーザーが常にコントロールを握り、すべてのトランザクションに対して明示的な同意を与えることを保証します。
React UIができたので、次はサーバーで重労働をこなす2つのAPIルートが必要です。
/api/verify/start
–
OpenID4VPリクエストを構築し、一度限りのチャレンジをMySQLに永続化し、すべてをブラウザに返します。/api/verify/finish
–
walletの応答を受け取り、チャレンジを検証し、クレデンシャルを検証&デコードし、最終的に簡潔なJSON結果をUIに返します。/api/verify/start
: OpenID4VPリクエストの生成#// 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️⃣ 短命でランダムなnonce(チャレンジ)を作成 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️⃣ 私たちが*欲しいもの*を記述するDCQLクエリを構築 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️⃣ ブラウザがnavigator.credentials.get()に渡せるオブジェクトを返す return NextResponse.json({ protocol: "openid4vp", // walletが使用するプロトコルをブラウザに伝える request: { dcql_query: dcqlQuery, // 何を提示するか nonce: challenge, // リプレイ攻撃対策 response_type: "vp_token", response_mode: "dc_api", // walletは直接/finishにPOSTする }, state: { credential_type: "mso_mdoc", // 後のチェックのために保持 nonce: challenge, challenge_id: challengeId, }, }); }
主要なパラメータ
• nonce
– リクエストとレスポンスを結びつける暗号学的チャレンジ(リプレイ攻撃を防ぐ)。•
dcql_query
– 私たちが必要とする正確なクレームを記述するオブジェクト。このガイドでは、まだ最終的な標準ではないものの、Digital
Credential Query Languageの最近のドラフトに触発されたdcql_query
構造を使用します。•
state
–
walletによってエコーバックされる任意のJSON。これによりDBレコードを検索できます。
ファイルsrc/lib/database.ts
は、チャレンジと検証セッションに関する基本的なMySQL操作(挿入、読み取り、使用済みマーク)をラップします。このロジックを単一のモジュールに保持することで、後でデータストアを交換するのが簡単になります。
/api/verify/finish
: プレゼンテーションの検証とデコード#// 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️⃣ 検証可能なプレゼンテーションの各部分を抽出 const vpTokenMap = body.vp_token ?? body.data?.vp_token; const state = body.state; const mdocToken = vpTokenMap?.cred1; // dcqlQueryでこのIDを要求した if (!vpTokenMap || !state || !mdocToken) { return NextResponse.json( { verified: false, message: "Malformed response" }, { status: 400 }, ); } // 2️⃣ 一度限りのチャレンジ検証 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️⃣ (疑似)暗号チェック – 本番では実際のmDL検証に置き換える // 実際のアプリケーションでは、専用のライブラリを使用して、発行者の公開鍵に対してmdoc署名の完全な // 暗号検証を実行します。 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️⃣ モバイルDL(mdoc)ペイロードを人間が読めるJSONにデコード 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, }); }
walletのレスポンスにおける重要なフィールド
• vp_token
–
walletが返す各クレデンシャルを保持するマップ。デモではvp_token.cred1
を取得します。•
state
–
/start
で提供したブロブのエコー。DBレコードを検索するためのnonce
を含みます。•
mdocToken
– ISO mDocを表すBase64URLエンコードされたCBOR構造体。
Verifierがブラウザからmdocクレデンシャルを受け取ると、それはCBORエンコードされたバイナリデータを含むBase64URL文字列です。実際のクレームを抽出するために、finish
エンドポイントはsrc/lib/crypto.ts
のヘルパー関数を使用して多段階のデコードプロセスを実行します。
decodeDigitalCredential
関数は、エンコードされた文字列から使用可能なオブジェクトへの変換を処理します。
// src/lib/crypto.ts export async function decodeDigitalCredential(encodedCredential: string) { // 1. Base64URLを標準のBase64に変換 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をバイナリにデコード const binaryString = atob(base64); const byteArray = Uint8Array.from(binaryString, (char) => char.charCodeAt(0)); // 3. CBORをデコード const decoded = await cbor.decodeFirst(byteArray); return decoded; }
cbor-web
ライブラリを使用して、バイナリデータを構造化されたJavaScriptオブジェクトにデコードします。decodeAllNamespaces
関数は、デコードされたCBORオブジェクトをさらに処理して、関連する名前空間から実際のクレームを抽出します。
// 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 (存在する場合): 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
)をデコードして、実際のクレーム値(名前、生年月日など)を抽出します。これらのステップを実行した後、finishエンドポイントは、mdocからのクレームを含む人間が読めるオブジェクトを取得します。例:
{ "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" }
このプロセスにより、Verifierは表示やさらなる処理のために、mdocクレデンシャルから必要な情報を安全かつ確実に抽出できます。
finishエンドポイントは、フロントエンドに最小限のJSONオブジェクトを返します。
{ "verified": true, "message": "mdoc credential verified successfully!", "credentialData": { "family_name": "Doe", "given_name": "John", "birth_date": "1990-01-01" } }
フロントエンドはstartVerification()
でこの応答を受け取り、それをReactの状態に保持するだけです。これにより、確認カードをレンダリングしたり、個々のクレームを表示したりできます。例:「ようこそ、John
Doeさん(1990-01-01生まれ)!」。
これで、ブラウザのネイティブなクレデンシャル処理機能を使用する、完全で動作するVerifierが完成しました。ここでは、それをローカルで実行する方法と、概念実証から本番対応アプリケーションに移行するためにできることを説明します。
リポジトリのクローン:
git clone https://github.com/corbado/digital-credentials-example.git cd digital-credentials-example
依存関係のインストール:
npm install
データベースの起動: マシンでDockerが実行されていることを確認し、MySQLコンテナを起動します。
docker-compose up -d
アプリケーションの実行:
npm run dev
ブラウザでhttp://localhost:3000
を開くと、VerifierのUIが表示されます。CMWalletを使用してQRコードをスキャンし、検証フローを完了させることができます。
このチュートリアルは、Verifierの基本的な構成要素を提供します。これを本番対応にするには、いくつかの追加機能を実装する必要があります。
完全な暗号検証:
現在の実装では、プレースホルダーチェック(mdocToken.length > 0
)を使用しています。実際のシナリオでは、Issuerの公開鍵に対してmdoc署名の完全な暗号検証を実行する必要があります(例:DIDを解決するか、公開鍵証明書を取得する)。DID解決標準については、W3C DID Resolution仕様を参照してください。
Issuerの失効確認: クレデンシャルは、有効期限前にIssuerによって失効されることがあります。本番のVerifierは、Issuerが提供する失効リストまたはステータスエンドポイントをクエリして、クレデンシャルのステータスを確認する必要があります。W3C Verifiable Credentials Status Listは、クレデンシャル失効リストの標準を提供します。
堅牢なエラーハンドリングとセキュリティ: 包括的なエラーハンドリング、入力検証、APIエンドポイントのレート制限を追加し、転送中のデータを保護するためにすべての通信がHTTPS(TLS)経由であることを確認します。OWASP API Security Guidelinesは、包括的なAPIセキュリティのベストプラクティスを提供します。
複数のクレデンシャルタイプのサポート:
ヨーロッパのDigital Identity(EUDI)PIDクレデンシャル以外にもクレデンシャルを受け取る予定がある場合は、異なるdoctype
値やクレデンシャル形式を処理するようにロジックを拡張します。W3C Verifiable Credentials Data Modelは、包括的なVC形式の仕様を提供します。
この例は、理解しやすくするために、意図的にブラウザを介したコアフローに焦点を当てています。以下のトピックはスコープ外と見なされます。
redirect_uri
を使用したwalletからバックエンドへの直接通信や、動的クライアント登録など、より複雑なOpenID4VP機能は実装していません。この基盤の上に構築し、これらの次のステップを取り入れることで、独自のアプリケーションでデジタルクレデンシャルを信頼し検証できる、堅牢で安全なVerifierを開発できます。
以上です!250行未満のTypeScriptで、エンドツーエンドのVerifierが完成しました。これは以下のことを行います。
本番環境では、プレースホルダーの検証を完全なISO 18013-5チェックに置き換え、Issuerの失効確認、レート制限、監査ログ、そしてもちろんエンドツーエンドのTLSを追加しますが、中心となる構成要素はまったく同じです。
このチュートリアルで使用または参照した主要なリソース、仕様、ツールの一部を以下に示します。
プロジェクトリポジトリ:
主要な仕様:
ツール:
ライブラリ:
Related Articles
Table of Contents